// Package project 内的 repository.go 是 sqlc 生成层之上的薄包装：把
// pgtype.* 与领域类型互转，把 pgx.ErrNoRows 翻译成 errs.NotFound，把
// pg unique_violation（编号竞争）暴露为可识别的 error 让 service 决定重试。
package project

import (
	"context"
	"errors"
	"time"

	"github.com/google/uuid"
	"github.com/jackc/pgerrcode"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgconn"
	"github.com/jackc/pgx/v5/pgtype"
	"github.com/jackc/pgx/v5/pgxpool"

	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
	projectsqlc "github.com/yan1h/agent-coding-workflow/internal/project/sqlc"
)

// Repository 是 project 模块对 PG 的全部依赖。所有方法都接受 context 并返回
// 领域类型。Service 层的单元测试通过手写 fakeRepo 满足该接口。
type Repository interface {
	// Project
	CreateProject(ctx context.Context, p *Project) (*Project, error)
	GetProjectBySlug(ctx context.Context, slug string) (*Project, error)
	GetProjectByID(ctx context.Context, id uuid.UUID) (*Project, error)
	ListProjects(ctx context.Context, includeArchived bool) ([]*Project, error)
	UpdateProject(ctx context.Context, p *Project) (*Project, error)
	ArchiveProject(ctx context.Context, id uuid.UUID) error
	UnarchiveProject(ctx context.Context, id uuid.UUID) error

	// ProjectMember 团队/角色 ACL（autonomy roadmap §11）
	// UpsertMember 新增或更新一名成员的角色（PK 冲突时更新 role）。
	UpsertMember(ctx context.Context, projectID, userID uuid.UUID, role Role, addedBy *uuid.UUID) (*Member, error)
	// DeleteMember 移除一名成员，返回受影响行数（0 表示不存在）。
	DeleteMember(ctx context.Context, projectID, userID uuid.UUID) (int64, error)
	// GetMemberRole 返回 userID 在 projectID 的成员角色；非成员返回 (RoleNone, nil)。
	GetMemberRole(ctx context.Context, projectID, userID uuid.UUID) (Role, error)
	// ListMembers 返回 projectID 的全部成员（按加入时间升序）。
	ListMembers(ctx context.Context, projectID uuid.UUID) ([]*Member, error)

	// Requirement
	CreateRequirement(ctx context.Context, r *Requirement) (*Requirement, error)
	GetRequirementByNumber(ctx context.Context, projectID uuid.UUID, number int) (*Requirement, error)
	GetRequirementByID(ctx context.Context, id uuid.UUID) (*Requirement, error)
	ListRequirements(ctx context.Context, projectID uuid.UUID, phase Phase, status Status) ([]*Requirement, error)
	UpdateRequirement(ctx context.Context, r *Requirement) (*Requirement, error)
	UpdateRequirementPhase(ctx context.Context, id uuid.UUID, to Phase) (*Requirement, error)
	CloseRequirement(ctx context.Context, id uuid.UUID) error
	ReopenRequirement(ctx context.Context, id uuid.UUID) error

	// Issue
	CreateIssue(ctx context.Context, i *Issue) (*Issue, error)
	GetIssueByNumber(ctx context.Context, projectID uuid.UUID, number int) (*Issue, error)
	GetIssueByID(ctx context.Context, id uuid.UUID) (*Issue, error)
	ListIssues(ctx context.Context, projectID uuid.UUID, filter IssueFilter, requirementID *uuid.UUID) ([]*Issue, error)
	ListIssuesByRequirement(ctx context.Context, requirementID uuid.UUID) ([]*Issue, error)
	UpdateIssueCore(ctx context.Context, id uuid.UUID, title, description string, requirementID *uuid.UUID, workspaceID *uuid.UUID) (*Issue, error)
	UpdateIssueAssignee(ctx context.Context, id uuid.UUID, assignee *uuid.UUID) (*Issue, error)
	UpdateIssuePriority(ctx context.Context, id uuid.UUID, priority int) (*Issue, error)
	CloseIssue(ctx context.Context, id uuid.UUID) error
	ReopenIssue(ctx context.Context, id uuid.UUID) error

	// Issue 依赖图（autonomy roadmap §10）
	CreateDependency(ctx context.Context, d *Dependency) (*Dependency, error)
	DeleteDependency(ctx context.Context, projectID, blockedID, blockerID uuid.UUID) (int64, error)
	// ListBlockers 返回阻塞 blockedID 的 issue 列表（其依赖）。
	ListBlockers(ctx context.Context, blockedID uuid.UUID) ([]*Issue, error)
	// ListBlocked 返回被 blockerID 阻塞的 issue 列表。
	ListBlocked(ctx context.Context, blockerID uuid.UUID) ([]*Issue, error)
	// ListBlockerIDs 返回 blockedID 的直接 blocker id（一跳），供 service 层环检查。
	ListBlockerIDs(ctx context.Context, blockedID uuid.UUID) ([]uuid.UUID, error)
	// ListSubtasks 返回 parentID 的全部子 issue。
	ListSubtasks(ctx context.Context, parentID uuid.UUID) ([]*Issue, error)
	// ListReadyLeafSubtasks 返回 project 下"就绪可派"的叶子子任务：open、有 parent、
	// 全部 blocker 已关闭、无活跃 acp_session、无 open 子。按 priority DESC, number ASC 排序。
	ListReadyLeafSubtasks(ctx context.Context, projectID uuid.UUID, limit int) ([]*Issue, error)
	// ListSchedulableProjects 返回当前至少有一个就绪叶子子任务的 project id（调度器预筛）。
	ListSchedulableProjects(ctx context.Context) ([]uuid.UUID, error)
	// CountActiveSessionsForProject 返回 project 内活跃（starting/running）acp_session 数。
	CountActiveSessionsForProject(ctx context.Context, projectID uuid.UUID) (int, error)

	// Artifact
	CreateArtifact(ctx context.Context, a *Artifact) (*Artifact, error)
	// ListArtifactsByRequirement 的 phase 传零值（""）表示不过滤阶段。
	ListArtifactsByRequirement(ctx context.Context, requirementID uuid.UUID, phase Phase) ([]*Artifact, error)
	GetArtifactByVersion(ctx context.Context, requirementID uuid.UUID, phase Phase, version int) (*Artifact, error)
	GetArtifactByID(ctx context.Context, id uuid.UUID) (*Artifact, error)
	GetMaxArtifactVersion(ctx context.Context, requirementID uuid.UUID, phase Phase) (int, error)
	// ApproveArtifact 给指定产物落 verdict（pass/fail/none），返回更新后的产物。
	ApproveArtifact(ctx context.Context, artifactID uuid.UUID, verdict Verdict) (*Artifact, error)

	// PhasePointer：每 (requirement, phase) 的已批准/当前产物指针。
	UpsertPhasePointer(ctx context.Context, requirementID uuid.UUID, phase Phase, artifactID uuid.UUID, approvedBy uuid.UUID) (*PhasePointer, error)
	GetPhasePointer(ctx context.Context, requirementID uuid.UUID, phase Phase) (*PhasePointer, error)
	ListPhasePointers(ctx context.Context, requirementID uuid.UUID) ([]*PhasePointer, error)
}

// IsUniqueViolation 报告 err 是否为 PG 唯一约束冲突。Service 层在 Create*
// 时碰到此 error 会重试 1 次（编号自增竞争）。
func IsUniqueViolation(err error) bool {
	var pg *pgconn.PgError
	return errors.As(err, &pg) && pg.Code == pgerrcode.UniqueViolation
}

// IsForeignKeyViolation 报告 err 是否为 PG 外键约束冲突。常见场景：
//   - issues.(project_id, workspace_id) 复合 FK 在跨 project 关联时被 PG 拒绝；
//   - requirements.workspace_id 关联到不存在的 workspace 时被 PG 拒绝。
//
// Repository 层据此把错误翻成 invalid_input（HTTP 400），避免泄漏 5xx。
func IsForeignKeyViolation(err error) bool {
	var pg *pgconn.PgError
	return errors.As(err, &pg) && pg.Code == pgerrcode.ForeignKeyViolation
}

type pgRepo struct{ q *projectsqlc.Queries }

// NewPostgresRepository 用现有的 pgxpool 构造 Repository。
func NewPostgresRepository(pool *pgxpool.Pool) Repository {
	return &pgRepo{q: projectsqlc.New(pool)}
}

// ===== 类型转换 helper =====

func toPgUUID(u uuid.UUID) pgtype.UUID { return pgtype.UUID{Bytes: u, Valid: true} }
func toPgUUIDPtr(u *uuid.UUID) pgtype.UUID {
	if u == nil {
		return pgtype.UUID{Valid: false}
	}
	return pgtype.UUID{Bytes: *u, Valid: true}
}
func fromPgUUID(p pgtype.UUID) uuid.UUID {
	if !p.Valid {
		return uuid.Nil
	}
	return uuid.UUID(p.Bytes)
}
func fromPgUUIDPtr(p pgtype.UUID) *uuid.UUID {
	if !p.Valid {
		return nil
	}
	u := uuid.UUID(p.Bytes)
	return &u
}
func fromPgTimePtr(t pgtype.Timestamptz) *time.Time {
	if !t.Valid {
		return nil
	}
	v := t.Time
	return &v
}

// ===== Project =====

func rowToProject(row projectsqlc.Project) *Project {
	return &Project{
		ID:          fromPgUUID(row.ID),
		Slug:        row.Slug,
		Name:        row.Name,
		Description: row.Description,
		Visibility:  Visibility(row.Visibility),
		OwnerID:     fromPgUUID(row.OwnerID),
		ArchivedAt:  fromPgTimePtr(row.ArchivedAt),
		CreatedAt:   row.CreatedAt.Time,
		UpdatedAt:   row.UpdatedAt.Time,
	}
}

func (r *pgRepo) CreateProject(ctx context.Context, p *Project) (*Project, error) {
	row, err := r.q.CreateProject(ctx, projectsqlc.CreateProjectParams{
		ID:          toPgUUID(p.ID),
		Slug:        p.Slug,
		Name:        p.Name,
		Description: p.Description,
		Visibility:  string(p.Visibility),
		OwnerID:     toPgUUID(p.OwnerID),
	})
	if err != nil {
		if IsUniqueViolation(err) {
			return nil, errs.New(errs.CodeProjectSlugTaken, "slug 已被使用")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "create project")
	}
	return rowToProject(row), nil
}

func (r *pgRepo) GetProjectBySlug(ctx context.Context, slug string) (*Project, error) {
	row, err := r.q.GetProjectBySlug(ctx, slug)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errs.New(errs.CodeNotFound, "project 不存在")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "get project by slug")
	}
	return rowToProject(row), nil
}

func (r *pgRepo) GetProjectByID(ctx context.Context, id uuid.UUID) (*Project, error) {
	row, err := r.q.GetProjectByID(ctx, toPgUUID(id))
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errs.New(errs.CodeNotFound, "project 不存在")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "get project by id")
	}
	return rowToProject(row), nil
}

func (r *pgRepo) ListProjects(ctx context.Context, includeArchived bool) ([]*Project, error) {
	rows, err := r.q.ListProjects(ctx, includeArchived)
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "list projects")
	}
	out := make([]*Project, 0, len(rows))
	for _, row := range rows {
		out = append(out, rowToProject(row))
	}
	return out, nil
}

func (r *pgRepo) UpdateProject(ctx context.Context, p *Project) (*Project, error) {
	row, err := r.q.UpdateProject(ctx, projectsqlc.UpdateProjectParams{
		ID:          toPgUUID(p.ID),
		Name:        p.Name,
		Description: p.Description,
		Visibility:  string(p.Visibility),
	})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errs.New(errs.CodeNotFound, "project 不存在")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "update project")
	}
	return rowToProject(row), nil
}

func (r *pgRepo) ArchiveProject(ctx context.Context, id uuid.UUID) error {
	if err := r.q.ArchiveProject(ctx, toPgUUID(id)); err != nil {
		return errs.Wrap(err, errs.CodeInternal, "archive project")
	}
	return nil
}

func (r *pgRepo) UnarchiveProject(ctx context.Context, id uuid.UUID) error {
	if err := r.q.UnarchiveProject(ctx, toPgUUID(id)); err != nil {
		return errs.Wrap(err, errs.CodeInternal, "unarchive project")
	}
	return nil
}

// ===== ProjectMember =====

func rowToMember(row projectsqlc.ProjectMember) *Member {
	return &Member{
		ProjectID: fromPgUUID(row.ProjectID),
		UserID:    fromPgUUID(row.UserID),
		Role:      Role(row.Role),
		AddedBy:   fromPgUUIDPtr(row.AddedBy),
		CreatedAt: row.CreatedAt.Time,
	}
}

func (r *pgRepo) UpsertMember(ctx context.Context, projectID, userID uuid.UUID, role Role, addedBy *uuid.UUID) (*Member, error) {
	row, err := r.q.UpsertProjectMember(ctx, projectsqlc.UpsertProjectMemberParams{
		ProjectID: toPgUUID(projectID),
		UserID:    toPgUUID(userID),
		Role:      string(role),
		AddedBy:   toPgUUIDPtr(addedBy),
	})
	if err != nil {
		// FK 冲突：user 不存在 → invalid_input（不泄漏 5xx）。
		if IsForeignKeyViolation(err) {
			return nil, errs.New(errs.CodeInvalidInput, "用户不存在")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "upsert project member")
	}
	return rowToMember(row), nil
}

func (r *pgRepo) DeleteMember(ctx context.Context, projectID, userID uuid.UUID) (int64, error) {
	n, err := r.q.DeleteProjectMember(ctx, projectsqlc.DeleteProjectMemberParams{
		ProjectID: toPgUUID(projectID),
		UserID:    toPgUUID(userID),
	})
	if err != nil {
		return 0, errs.Wrap(err, errs.CodeInternal, "delete project member")
	}
	return n, nil
}

func (r *pgRepo) GetMemberRole(ctx context.Context, projectID, userID uuid.UUID) (Role, error) {
	row, err := r.q.GetProjectMember(ctx, projectsqlc.GetProjectMemberParams{
		ProjectID: toPgUUID(projectID),
		UserID:    toPgUUID(userID),
	})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return RoleNone, nil
		}
		return RoleNone, errs.Wrap(err, errs.CodeInternal, "get project member role")
	}
	return Role(row.Role), nil
}

func (r *pgRepo) ListMembers(ctx context.Context, projectID uuid.UUID) ([]*Member, error) {
	rows, err := r.q.ListProjectMembers(ctx, toPgUUID(projectID))
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "list project members")
	}
	out := make([]*Member, 0, len(rows))
	for _, row := range rows {
		out = append(out, rowToMember(row))
	}
	return out, nil
}

// ===== Requirement =====

func rowToRequirement(row projectsqlc.Requirement) *Requirement {
	return &Requirement{
		ID:          fromPgUUID(row.ID),
		ProjectID:   fromPgUUID(row.ProjectID),
		WorkspaceID: fromPgUUIDPtr(row.WorkspaceID),
		Number:      int(row.Number),
		Title:       row.Title,
		Description: row.Description,
		Phase:       Phase(row.Phase),
		Status:      Status(row.Status),
		OwnerID:     fromPgUUID(row.OwnerID),
		ClosedAt:    fromPgTimePtr(row.ClosedAt),
		CreatedAt:   row.CreatedAt.Time,
		UpdatedAt:   row.UpdatedAt.Time,
	}
}

func (r *pgRepo) CreateRequirement(ctx context.Context, in *Requirement) (*Requirement, error) {
	row, err := r.q.CreateRequirement(ctx, projectsqlc.CreateRequirementParams{
		ID:          toPgUUID(in.ID),
		ProjectID:   toPgUUID(in.ProjectID),
		Title:       in.Title,
		Description: in.Description,
		OwnerID:     toPgUUID(in.OwnerID),
		WorkspaceID: toPgUUIDPtr(in.WorkspaceID),
	})
	if err != nil {
		if IsUniqueViolation(err) {
			return nil, err // service 层据此重试一次
		}
		if IsForeignKeyViolation(err) {
			return nil, errs.Wrap(err, errs.CodeInvalidInput, "workspace 与 project 不匹配或不存在")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "create requirement")
	}
	return rowToRequirement(row), nil
}

func (r *pgRepo) GetRequirementByNumber(ctx context.Context, projectID uuid.UUID, number int) (*Requirement, error) {
	row, err := r.q.GetRequirementByNumber(ctx, projectsqlc.GetRequirementByNumberParams{
		ProjectID: toPgUUID(projectID),
		Number:    int32(number), //nolint:gosec // number 来自 service 层 number int 字段，原值即来自 DB int32，溢出不可达
	})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errs.New(errs.CodeNotFound, "requirement 不存在")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "get requirement by number")
	}
	return rowToRequirement(row), nil
}

func (r *pgRepo) GetRequirementByID(ctx context.Context, id uuid.UUID) (*Requirement, error) {
	row, err := r.q.GetRequirementByID(ctx, toPgUUID(id))
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errs.New(errs.CodeNotFound, "requirement 不存在")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "get requirement by id")
	}
	return rowToRequirement(row), nil
}

func (r *pgRepo) ListRequirements(ctx context.Context, projectID uuid.UUID, phase Phase, status Status) ([]*Requirement, error) {
	var phasePtr, statusPtr *string
	if phase != "" {
		s := string(phase)
		phasePtr = &s
	}
	if status != "" {
		s := string(status)
		statusPtr = &s
	}
	rows, err := r.q.ListRequirements(ctx, projectsqlc.ListRequirementsParams{
		ProjectID: toPgUUID(projectID),
		Phase:     phasePtr,
		Status:    statusPtr,
	})
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "list requirements")
	}
	out := make([]*Requirement, 0, len(rows))
	for _, row := range rows {
		out = append(out, rowToRequirement(row))
	}
	return out, nil
}

func (r *pgRepo) UpdateRequirement(ctx context.Context, in *Requirement) (*Requirement, error) {
	row, err := r.q.UpdateRequirement(ctx, projectsqlc.UpdateRequirementParams{
		ID:          toPgUUID(in.ID),
		Title:       in.Title,
		Description: in.Description,
		OwnerID:     toPgUUID(in.OwnerID),
		WorkspaceID: toPgUUIDPtr(in.WorkspaceID),
	})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errs.New(errs.CodeNotFound, "requirement 不存在")
		}
		if IsForeignKeyViolation(err) {
			return nil, errs.Wrap(err, errs.CodeInvalidInput, "workspace 与 project 不匹配或不存在")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "update requirement")
	}
	return rowToRequirement(row), nil
}

func (r *pgRepo) UpdateRequirementPhase(ctx context.Context, id uuid.UUID, to Phase) (*Requirement, error) {
	row, err := r.q.UpdateRequirementPhase(ctx, projectsqlc.UpdateRequirementPhaseParams{
		ID:    toPgUUID(id),
		Phase: string(to),
	})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errs.New(errs.CodeNotFound, "requirement 不存在")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "update requirement phase")
	}
	return rowToRequirement(row), nil
}

func (r *pgRepo) CloseRequirement(ctx context.Context, id uuid.UUID) error {
	if err := r.q.CloseRequirement(ctx, toPgUUID(id)); err != nil {
		return errs.Wrap(err, errs.CodeInternal, "close requirement")
	}
	return nil
}

func (r *pgRepo) ReopenRequirement(ctx context.Context, id uuid.UUID) error {
	if err := r.q.ReopenRequirement(ctx, toPgUUID(id)); err != nil {
		return errs.Wrap(err, errs.CodeInternal, "reopen requirement")
	}
	return nil
}

// ===== Issue =====

func rowToIssue(row projectsqlc.Issue) *Issue {
	return &Issue{
		ID:            fromPgUUID(row.ID),
		ProjectID:     fromPgUUID(row.ProjectID),
		RequirementID: fromPgUUIDPtr(row.RequirementID),
		WorkspaceID:   fromPgUUIDPtr(row.WorkspaceID),
		Number:        int(row.Number),
		Title:         row.Title,
		Description:   row.Description,
		Status:        Status(row.Status),
		AssigneeID:    fromPgUUIDPtr(row.AssigneeID),
		CreatedBy:     fromPgUUID(row.CreatedBy),
		ClosedAt:      fromPgTimePtr(row.ClosedAt),
		CreatedAt:     row.CreatedAt.Time,
		UpdatedAt:     row.UpdatedAt.Time,
		ParentID:      fromPgUUIDPtr(row.ParentID),
		Priority:      int(row.Priority),
	}
}

func (r *pgRepo) CreateIssue(ctx context.Context, in *Issue) (*Issue, error) {
	row, err := r.q.CreateIssue(ctx, projectsqlc.CreateIssueParams{
		ID:            toPgUUID(in.ID),
		ProjectID:     toPgUUID(in.ProjectID),
		RequirementID: toPgUUIDPtr(in.RequirementID),
		Title:         in.Title,
		Description:   in.Description,
		AssigneeID:    toPgUUIDPtr(in.AssigneeID),
		CreatedBy:     toPgUUID(in.CreatedBy),
		WorkspaceID:   toPgUUIDPtr(in.WorkspaceID),
		ParentID:      toPgUUIDPtr(in.ParentID),
		Priority:      int32(in.Priority), //nolint:gosec // priority 0..3，CHECK 约束兜底
	})
	if err != nil {
		if IsUniqueViolation(err) {
			return nil, err
		}
		if IsForeignKeyViolation(err) {
			return nil, errs.Wrap(err, errs.CodeInvalidInput, "workspace 与 project 不匹配或不存在")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "create issue")
	}
	return rowToIssue(row), nil
}

func (r *pgRepo) GetIssueByNumber(ctx context.Context, projectID uuid.UUID, number int) (*Issue, error) {
	row, err := r.q.GetIssueByNumber(ctx, projectsqlc.GetIssueByNumberParams{
		ProjectID: toPgUUID(projectID),
		Number:    int32(number), //nolint:gosec // 同上：number 原始来自 DB int32
	})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errs.New(errs.CodeNotFound, "issue 不存在")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "get issue by number")
	}
	return rowToIssue(row), nil
}

func (r *pgRepo) GetIssueByID(ctx context.Context, id uuid.UUID) (*Issue, error) {
	row, err := r.q.GetIssueByID(ctx, toPgUUID(id))
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errs.New(errs.CodeNotFound, "issue 不存在")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "get issue by id")
	}
	return rowToIssue(row), nil
}

func (r *pgRepo) ListIssues(ctx context.Context, projectID uuid.UUID, filter IssueFilter, requirementID *uuid.UUID) ([]*Issue, error) {
	var statusPtr, reqFilter *string
	if filter.Status != "" {
		s := string(filter.Status)
		statusPtr = &s
	}
	switch {
	case filter.Requirement == nil:
		reqFilter = nil
	case *filter.Requirement == 0:
		s := "none"
		reqFilter = &s
	default:
		s := "id"
		reqFilter = &s
	}
	rows, err := r.q.ListIssues(ctx, projectsqlc.ListIssuesParams{
		ProjectID:         toPgUUID(projectID),
		Status:            statusPtr,
		RequirementFilter: reqFilter,
		RequirementID:     toPgUUIDPtr(requirementID),
		AssigneeID:        toPgUUIDPtr(filter.AssigneeID),
	})
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "list issues")
	}
	out := make([]*Issue, 0, len(rows))
	for _, row := range rows {
		out = append(out, rowToIssue(row))
	}
	return out, nil
}

func (r *pgRepo) ListIssuesByRequirement(ctx context.Context, requirementID uuid.UUID) ([]*Issue, error) {
	rows, err := r.q.ListIssuesByRequirement(ctx, toPgUUID(requirementID))
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "list issues by requirement")
	}
	out := make([]*Issue, 0, len(rows))
	for _, row := range rows {
		out = append(out, rowToIssue(row))
	}
	return out, nil
}

func (r *pgRepo) UpdateIssueCore(ctx context.Context, id uuid.UUID, title, description string, requirementID *uuid.UUID, workspaceID *uuid.UUID) (*Issue, error) {
	row, err := r.q.UpdateIssueCore(ctx, projectsqlc.UpdateIssueCoreParams{
		ID:            toPgUUID(id),
		Title:         title,
		Description:   description,
		RequirementID: toPgUUIDPtr(requirementID),
		WorkspaceID:   toPgUUIDPtr(workspaceID),
	})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errs.New(errs.CodeNotFound, "issue 不存在")
		}
		if IsForeignKeyViolation(err) {
			return nil, errs.Wrap(err, errs.CodeInvalidInput, "workspace 与 project 不匹配或不存在")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "update issue core")
	}
	return rowToIssue(row), nil
}

func (r *pgRepo) UpdateIssueAssignee(ctx context.Context, id uuid.UUID, assignee *uuid.UUID) (*Issue, error) {
	row, err := r.q.UpdateIssueAssignee(ctx, projectsqlc.UpdateIssueAssigneeParams{
		ID:         toPgUUID(id),
		AssigneeID: toPgUUIDPtr(assignee),
	})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errs.New(errs.CodeNotFound, "issue 不存在")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "update issue assignee")
	}
	return rowToIssue(row), nil
}

func (r *pgRepo) CloseIssue(ctx context.Context, id uuid.UUID) error {
	if err := r.q.CloseIssue(ctx, toPgUUID(id)); err != nil {
		return errs.Wrap(err, errs.CodeInternal, "close issue")
	}
	return nil
}

func (r *pgRepo) ReopenIssue(ctx context.Context, id uuid.UUID) error {
	if err := r.q.ReopenIssue(ctx, toPgUUID(id)); err != nil {
		return errs.Wrap(err, errs.CodeInternal, "reopen issue")
	}
	return nil
}

func (r *pgRepo) UpdateIssuePriority(ctx context.Context, id uuid.UUID, priority int) (*Issue, error) {
	row, err := r.q.UpdateIssuePriority(ctx, projectsqlc.UpdateIssuePriorityParams{
		ID:       toPgUUID(id),
		Priority: int32(priority), //nolint:gosec // priority 0..3，CHECK 约束兜底
	})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errs.New(errs.CodeNotFound, "issue 不存在")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "update issue priority")
	}
	return rowToIssue(row), nil
}

// ===== Issue 依赖图（autonomy roadmap §10）=====

func rowToDependency(row projectsqlc.IssueDependency) *Dependency {
	return &Dependency{
		ID:        fromPgUUID(row.ID),
		ProjectID: fromPgUUID(row.ProjectID),
		BlockedID: fromPgUUID(row.BlockedID),
		BlockerID: fromPgUUID(row.BlockerID),
		CreatedBy: fromPgUUID(row.CreatedBy),
		CreatedAt: row.CreatedAt.Time,
	}
}

func (r *pgRepo) CreateDependency(ctx context.Context, d *Dependency) (*Dependency, error) {
	row, err := r.q.CreateDependency(ctx, projectsqlc.CreateDependencyParams{
		ID:        toPgUUID(d.ID),
		ProjectID: toPgUUID(d.ProjectID),
		BlockedID: toPgUUID(d.BlockedID),
		BlockerID: toPgUUID(d.BlockerID),
		CreatedBy: toPgUUID(d.CreatedBy),
	})
	if err != nil {
		if IsUniqueViolation(err) {
			return nil, errs.New(errs.CodeInvalidInput, "依赖已存在")
		}
		if IsForeignKeyViolation(err) {
			return nil, errs.Wrap(err, errs.CodeInvalidInput, "依赖两端 issue 不存在或跨 project")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "create dependency")
	}
	return rowToDependency(row), nil
}

func (r *pgRepo) DeleteDependency(ctx context.Context, projectID, blockedID, blockerID uuid.UUID) (int64, error) {
	n, err := r.q.DeleteDependency(ctx, projectsqlc.DeleteDependencyParams{
		ProjectID: toPgUUID(projectID),
		BlockedID: toPgUUID(blockedID),
		BlockerID: toPgUUID(blockerID),
	})
	if err != nil {
		return 0, errs.Wrap(err, errs.CodeInternal, "delete dependency")
	}
	return n, nil
}

func (r *pgRepo) ListBlockers(ctx context.Context, blockedID uuid.UUID) ([]*Issue, error) {
	rows, err := r.q.ListBlockers(ctx, toPgUUID(blockedID))
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "list blockers")
	}
	out := make([]*Issue, 0, len(rows))
	for _, row := range rows {
		out = append(out, rowToIssue(row))
	}
	return out, nil
}

func (r *pgRepo) ListBlocked(ctx context.Context, blockerID uuid.UUID) ([]*Issue, error) {
	rows, err := r.q.ListBlocked(ctx, toPgUUID(blockerID))
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "list blocked")
	}
	out := make([]*Issue, 0, len(rows))
	for _, row := range rows {
		out = append(out, rowToIssue(row))
	}
	return out, nil
}

func (r *pgRepo) ListBlockerIDs(ctx context.Context, blockedID uuid.UUID) ([]uuid.UUID, error) {
	rows, err := r.q.ListBlockerIDs(ctx, toPgUUID(blockedID))
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "list blocker ids")
	}
	out := make([]uuid.UUID, 0, len(rows))
	for _, p := range rows {
		out = append(out, fromPgUUID(p))
	}
	return out, nil
}

func (r *pgRepo) ListSubtasks(ctx context.Context, parentID uuid.UUID) ([]*Issue, error) {
	rows, err := r.q.ListSubtasks(ctx, toPgUUID(parentID))
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "list subtasks")
	}
	out := make([]*Issue, 0, len(rows))
	for _, row := range rows {
		out = append(out, rowToIssue(row))
	}
	return out, nil
}

func (r *pgRepo) ListReadyLeafSubtasks(ctx context.Context, projectID uuid.UUID, limit int) ([]*Issue, error) {
	if limit <= 0 {
		limit = 50
	}
	rows, err := r.q.ListReadyLeafSubtasks(ctx, projectsqlc.ListReadyLeafSubtasksParams{
		ProjectID: toPgUUID(projectID),
		Limit:     int32(limit), //nolint:gosec // limit 来自配置/调用方，正向小整数
	})
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "list ready leaf subtasks")
	}
	out := make([]*Issue, 0, len(rows))
	for _, row := range rows {
		out = append(out, rowToIssue(row))
	}
	return out, nil
}

func (r *pgRepo) ListSchedulableProjects(ctx context.Context) ([]uuid.UUID, error) {
	rows, err := r.q.ListSchedulableProjects(ctx)
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "list schedulable projects")
	}
	out := make([]uuid.UUID, 0, len(rows))
	for _, p := range rows {
		out = append(out, fromPgUUID(p))
	}
	return out, nil
}

func (r *pgRepo) CountActiveSessionsForProject(ctx context.Context, projectID uuid.UUID) (int, error) {
	n, err := r.q.CountActiveSessionsForProject(ctx, toPgUUID(projectID))
	if err != nil {
		return 0, errs.Wrap(err, errs.CodeInternal, "count active sessions for project")
	}
	return int(n), nil
}

// ===== Artifact =====

func rowToArtifact(row projectsqlc.RequirementArtifact) *Artifact {
	return &Artifact{
		ID:              fromPgUUID(row.ID),
		RequirementID:   fromPgUUID(row.RequirementID),
		Phase:           Phase(row.Phase),
		Version:         int(row.Version),
		Content:         row.Content,
		Note:            row.Note,
		SourceMessageID: row.SourceMessageID,
		CreatedBy:       fromPgUUID(row.CreatedBy),
		CreatedAt:       row.CreatedAt.Time,
		Verdict:         Verdict(row.Verdict),
	}
}

func rowToPhasePointer(row projectsqlc.RequirementPhasePointer) *PhasePointer {
	return &PhasePointer{
		RequirementID:      fromPgUUID(row.RequirementID),
		Phase:              Phase(row.Phase),
		ApprovedArtifactID: fromPgUUIDPtr(row.ApprovedArtifactID),
		ApprovedBy:         fromPgUUIDPtr(row.ApprovedBy),
		ApprovedAt:         fromPgTimePtr(row.ApprovedAt),
	}
}

func (r *pgRepo) CreateArtifact(ctx context.Context, in *Artifact) (*Artifact, error) {
	row, err := r.q.CreateRequirementArtifact(ctx, projectsqlc.CreateRequirementArtifactParams{
		ID:              toPgUUID(in.ID),
		RequirementID:   toPgUUID(in.RequirementID),
		Phase:           string(in.Phase),
		Version:         int32(in.Version), //nolint:gosec // version 来自 max+1，正常区间不溢出
		Content:         in.Content,
		Note:            in.Note,
		SourceMessageID: in.SourceMessageID,
		CreatedBy:       toPgUUID(in.CreatedBy),
	})
	if err != nil {
		if IsUniqueViolation(err) {
			return nil, err // service 层据此重试一次
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "create artifact")
	}
	return rowToArtifact(row), nil
}

func (r *pgRepo) ListArtifactsByRequirement(ctx context.Context, requirementID uuid.UUID, phase Phase) ([]*Artifact, error) {
	rows, err := r.q.ListRequirementArtifactsByRequirement(ctx, projectsqlc.ListRequirementArtifactsByRequirementParams{
		RequirementID: toPgUUID(requirementID),
		Column2:       string(phase),
	})
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "list artifacts by requirement")
	}
	out := make([]*Artifact, 0, len(rows))
	for _, row := range rows {
		out = append(out, rowToArtifact(row))
	}
	return out, nil
}

func (r *pgRepo) GetArtifactByVersion(ctx context.Context, requirementID uuid.UUID, phase Phase, version int) (*Artifact, error) {
	row, err := r.q.GetRequirementArtifactByVersion(ctx, projectsqlc.GetRequirementArtifactByVersionParams{
		RequirementID: toPgUUID(requirementID),
		Phase:         string(phase),
		Version:       int32(version), //nolint:gosec // version 来自 service 层 int，原值即来自 DB int32
	})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errs.New(errs.CodeNotFound, "artifact 不存在")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "get artifact by version")
	}
	return rowToArtifact(row), nil
}

func (r *pgRepo) GetMaxArtifactVersion(ctx context.Context, requirementID uuid.UUID, phase Phase) (int, error) {
	max, err := r.q.GetMaxRequirementArtifactVersion(ctx, projectsqlc.GetMaxRequirementArtifactVersionParams{
		RequirementID: toPgUUID(requirementID),
		Phase:         string(phase),
	})
	if err != nil {
		return 0, errs.Wrap(err, errs.CodeInternal, "get max artifact version")
	}
	return int(max), nil
}

func (r *pgRepo) GetArtifactByID(ctx context.Context, id uuid.UUID) (*Artifact, error) {
	row, err := r.q.GetRequirementArtifactByID(ctx, toPgUUID(id))
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errs.New(errs.CodeNotFound, "artifact 不存在")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "get artifact by id")
	}
	return rowToArtifact(row), nil
}

func (r *pgRepo) ApproveArtifact(ctx context.Context, artifactID uuid.UUID, verdict Verdict) (*Artifact, error) {
	row, err := r.q.ApproveRequirementArtifact(ctx, projectsqlc.ApproveRequirementArtifactParams{
		ID:      toPgUUID(artifactID),
		Verdict: string(verdict),
	})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errs.New(errs.CodeNotFound, "artifact 不存在")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "approve artifact")
	}
	return rowToArtifact(row), nil
}

// ===== PhasePointer =====

func (r *pgRepo) UpsertPhasePointer(ctx context.Context, requirementID uuid.UUID, phase Phase, artifactID, approvedBy uuid.UUID) (*PhasePointer, error) {
	row, err := r.q.UpsertRequirementPhasePointer(ctx, projectsqlc.UpsertRequirementPhasePointerParams{
		RequirementID:      toPgUUID(requirementID),
		Phase:              string(phase),
		ApprovedArtifactID: toPgUUID(artifactID),
		ApprovedBy:         toPgUUID(approvedBy),
	})
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "upsert phase pointer")
	}
	return rowToPhasePointer(row), nil
}

func (r *pgRepo) GetPhasePointer(ctx context.Context, requirementID uuid.UUID, phase Phase) (*PhasePointer, error) {
	row, err := r.q.GetRequirementPhasePointer(ctx, projectsqlc.GetRequirementPhasePointerParams{
		RequirementID: toPgUUID(requirementID),
		Phase:         string(phase),
	})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errs.New(errs.CodeNotFound, "phase pointer 不存在")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "get phase pointer")
	}
	return rowToPhasePointer(row), nil
}

func (r *pgRepo) ListPhasePointers(ctx context.Context, requirementID uuid.UUID) ([]*PhasePointer, error) {
	rows, err := r.q.ListRequirementPhasePointers(ctx, toPgUUID(requirementID))
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "list phase pointers")
	}
	out := make([]*PhasePointer, 0, len(rows))
	for _, row := range rows {
		out = append(out, rowToPhasePointer(row))
	}
	return out, nil
}
