package tech.easyflow.skill.mapper;

import com.mybatisflex.core.BaseMapper;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import tech.easyflow.skill.entity.SkillContent;

import java.util.Date;
import java.util.List;

/**
 * Skill 二进制内容 Mapper。
 */
public interface SkillContentMapper extends BaseMapper<SkillContent> {

    /**
     * 原子增加与内容引用、大小及哈希均匹配的正式内容引用计数。
     *
     * <p>storage_locator 允许为 null 仅用于兼容迁移前正式内容；空定位符与旧 PENDING 路径
     * 均不会被视为活动内容。</p>
     *
     * @param contentRef 内容引用
     * @param size 内容字节数
     * @return 更新行数
     */
    @Update("UPDATE tb_skill_content SET ref_count=ref_count+1,modified=CURRENT_TIMESTAMP "
            + "WHERE content_ref=#{contentRef} AND size=#{size} "
            + "AND CONCAT('sha256:',content_hash)=#{contentRef} AND ref_count>0 "
            + "AND file_path IS NOT NULL AND file_path<>'' AND file_path NOT LIKE '__PENDING__:%' "
            + "AND (storage_locator IS NULL OR storage_locator<>'')")
    int retainMatching(@Param("contentRef") String contentRef, @Param("size") long size);

    /**
     * 以当前读方式锁定并返回指定内容索引。
     *
     * <p>该查询用于引用计数状态转换，避免 MySQL REPEATABLE READ 下普通一致性读反复返回
     * 调用方事务早先建立的旧快照。</p>
     *
     * @param contentRef 内容引用
     * @return 当前内容索引；不存在时为 null
     */
    @Select("SELECT content_ref AS contentRef,content_hash AS contentHash,file_path AS filePath," +
            "storage_locator AS storageLocator,media_type AS mediaType,size,ref_count AS refCount,created,modified "
            + "FROM tb_skill_content WHERE content_ref=#{contentRef} FOR UPDATE")
    SkillContent selectForUpdate(@Param("contentRef") String contentRef);

    /**
     * 将已经完成物理校验的旧版零引用内容恢复为一份活动引用。
     *
     * <p>仅允许恢复缺少稳定定位符的迁移前内容；读取路径、哈希、大小与零引用状态均须保持
     * 锁定读取时的值，避免复活正在由新流程清理的可恢复对象。</p>
     *
     * @param contentRef 内容引用
     * @param contentHash 内容哈希
     * @param filePath 已校验的旧版读取路径
     * @param size 内容字节数
     * @return 成功恢复为 1，状态已变化为 0
     */
    @Update("UPDATE tb_skill_content SET ref_count=1,modified=CURRENT_TIMESTAMP "
            + "WHERE content_ref=#{contentRef} AND content_hash=#{contentHash} "
            + "AND file_path=#{filePath} AND size=#{size} AND ref_count=0 "
            + "AND storage_locator IS NULL AND file_path IS NOT NULL AND file_path<>'' "
            + "AND file_path NOT LIKE '__PENDING__:%'")
    int resurrectVerifiedLegacy(@Param("contentRef") String contentRef,
                                @Param("contentHash") String contentHash,
                                @Param("filePath") String filePath,
                                @Param("size") long size);

    /**
     * 插入首个引用已经激活的正式内容索引。
     *
     * <p>新流程必须同时提供非空读取路径和稳定存储定位符，并保证内容引用与哈希一致。</p>
     *
     * @param contentRef 内容引用
     * @param contentHash 内容哈希
     * @param filePath 文件读取路径
     * @param storageLocator 稳定存储定位符
     * @param mediaType 媒体类型
     * @param size 内容字节数
     * @return 成功插入为 1，参数不满足活动内容约束为 0
     * @throws org.springframework.dao.DuplicateKeyException 内容引用已经存在
     */
    @Insert("INSERT INTO tb_skill_content("
            + "content_ref,content_hash,file_path,storage_locator,media_type,size,ref_count,created,modified) "
            + "SELECT #{contentRef},#{contentHash},#{filePath},#{storageLocator},#{mediaType},#{size},"
            + "1,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP "
            + "WHERE #{filePath} IS NOT NULL AND #{filePath}<>'' "
            + "AND #{filePath} NOT LIKE '__PENDING__:%' "
            + "AND #{storageLocator} IS NOT NULL AND #{storageLocator}<>'' "
            + "AND #{size}>=0 AND CONCAT('sha256:',#{contentHash})=#{contentRef}")
    int insertActive(@Param("contentRef") String contentRef,
                     @Param("contentHash") String contentHash,
                     @Param("filePath") String filePath,
                     @Param("storageLocator") String storageLocator,
                     @Param("mediaType") String mediaType,
                     @Param("size") long size);

    /**
     * 原子增加内容引用计数。
     *
     * @param contentRef 内容引用
     * @return 更新行数
     */
    @Update("UPDATE tb_skill_content SET ref_count = ref_count + 1, modified = CURRENT_TIMESTAMP "
            + "WHERE content_ref = #{contentRef} AND ref_count > 0 "
            + "AND CONCAT('sha256:',content_hash)=#{contentRef} "
            + "AND file_path IS NOT NULL AND file_path<>'' AND file_path NOT LIKE '__PENDING__:%' "
            + "AND (storage_locator IS NULL OR storage_locator<>'')")
    int retain(String contentRef);

    /**
     * 原子减少仍有多个持有者的内容引用计数。
     *
     * @param contentRef 内容引用
     * @return 更新行数
     */
    @Update("UPDATE tb_skill_content SET ref_count = ref_count - 1, modified = CURRENT_TIMESTAMP "
            + "WHERE content_ref = #{contentRef} AND ref_count > 1 "
            + "AND file_path IS NOT NULL AND file_path<>'' AND file_path NOT LIKE '__PENDING__:%' "
            + "AND (storage_locator IS NULL OR storage_locator<>'')")
    int releaseShared(String contentRef);

    /**
     * 通过 INSERT IGNORE 原子抢占新内容 hash，避免跨实例 get-then-insert 竞态。
     *
     * @param contentRef 内容引用
     * @param contentHash 内容 hash
     * @param pendingPath 临时占位路径
     * @param mediaType 媒体类型
     * @param size 字节数
     * @return 抢占成功为 1，已有内容为 0
     */
    @Insert("INSERT IGNORE INTO tb_skill_content(content_ref,content_hash,file_path,media_type,size,ref_count,created,modified) "
            + "VALUES(#{contentRef},#{contentHash},#{pendingPath},#{mediaType},#{size},0,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)")
    int reserve(@Param("contentRef") String contentRef,
                @Param("contentHash") String contentHash,
                @Param("pendingPath") String pendingPath,
                @Param("mediaType") String mediaType,
                @Param("size") long size);

    /**
     * 完成内容物理路径写入。
     *
     * @param contentRef 内容引用
     * @param filePath 物理路径
     * @return 更新行数
     */
    @Update("UPDATE tb_skill_content SET file_path=#{filePath}, ref_count=1, modified=CURRENT_TIMESTAMP "
            + "WHERE content_ref=#{contentRef} AND ref_count=0 AND file_path LIKE '__PENDING__:%'")
    int finishReservation(@Param("contentRef") String contentRef, @Param("filePath") String filePath);

    /**
     * 按读取路径和稳定定位符精确标记最后一份正式内容引用为待清理。
     *
     * @param contentRef 内容引用
     * @param filePath 当前文件读取路径
     * @param storageLocator 当前稳定存储定位符
     * @return 更新行数
     */
    @Update("UPDATE tb_skill_content SET ref_count=0,modified=CURRENT_TIMESTAMP "
            + "WHERE content_ref=#{contentRef} AND file_path=#{filePath} "
            + "AND storage_locator<=>#{storageLocator} AND ref_count=1 "
            + "AND file_path IS NOT NULL AND file_path<>'' AND file_path NOT LIKE '__PENDING__:%' "
            + "AND (storage_locator IS NULL OR storage_locator<>'')")
    int markReleased(@Param("contentRef") String contentRef,
                     @Param("filePath") String filePath,
                     @Param("storageLocator") String storageLocator);

    /**
     * 统计当前可读取的正式内容。
     *
     * @param contentRef 内容引用
     * @return 可见内容数量
     */
    @Select("SELECT COUNT(1) FROM tb_skill_content WHERE content_ref=#{contentRef} "
            + "AND ref_count>0 AND file_path IS NOT NULL AND file_path<>'' "
            + "AND file_path NOT LIKE '__PENDING__:%' "
            + "AND (storage_locator IS NULL OR storage_locator<>'')")
    int countVisible(String contentRef);

    /**
     * 查询超过保留期限的未完成占位记录。
     *
     * @param cutoff 截止时间
     * @param limit 最大返回数量
     * @return 待清理占位记录
     */
    @Select("SELECT content_ref AS contentRef,content_hash AS contentHash,file_path AS filePath," +
            "storage_locator AS storageLocator,media_type AS mediaType,size,ref_count AS refCount,created,modified "
            + "FROM tb_skill_content WHERE ref_count=0 AND file_path LIKE '__PENDING__:%' "
            + "AND modified < #{cutoff} ORDER BY modified ASC LIMIT #{limit}")
    List<SkillContent> findStalePending(@Param("cutoff") Date cutoff, @Param("limit") int limit);

    /**
     * 条件删除仍处于原占位状态的过期记录。
     *
     * @param contentRef 内容引用
     * @param pendingPath 原占位路径
     * @param cutoff 截止时间
     * @return 删除行数
     */
    @Delete("DELETE FROM tb_skill_content WHERE content_ref=#{contentRef} AND file_path=#{pendingPath} "
            + "AND ref_count=0 AND file_path LIKE '__PENDING__:%' AND modified < #{cutoff}")
    int deleteStalePending(@Param("contentRef") String contentRef,
                           @Param("pendingPath") String pendingPath,
                           @Param("cutoff") Date cutoff);

    /**
     * 查询需要重试物理删除的零引用内容。
     *
     * @param cutoff 截止时间
     * @param limit 最大返回数量
     * @return 待清理内容
     */
    @Select("SELECT content_ref AS contentRef,content_hash AS contentHash,file_path AS filePath," +
            "storage_locator AS storageLocator,media_type AS mediaType,size,ref_count AS refCount,created,modified "
            + "FROM tb_skill_content WHERE ref_count=0 AND file_path NOT LIKE '__PENDING__:%' "
            + "AND storage_locator IS NOT NULL AND storage_locator<>'' "
            + "AND modified < #{cutoff} ORDER BY modified ASC LIMIT #{limit}")
    List<SkillContent> findReleasedBefore(@Param("cutoff") Date cutoff, @Param("limit") int limit);

    /**
     * 按读取路径和稳定定位符精确删除已完成物理清理的零引用索引。
     *
     * @param contentRef 内容引用
     * @param filePath 原文件读取路径
     * @param storageLocator 原稳定存储定位符
     * @return 删除行数
     */
    @Delete("DELETE FROM tb_skill_content WHERE content_ref=#{contentRef} "
            + "AND file_path=#{filePath} AND storage_locator<=>#{storageLocator} AND ref_count=0")
    int deleteReleased(@Param("contentRef") String contentRef,
                       @Param("filePath") String filePath,
                       @Param("storageLocator") String storageLocator);
}
