// Package project 内的 issue_service.go 实装 IssueService。
//
// 校验要点：
//   - requirement 必须与 issue 同 project（fakeRepo / sql 都按 project_id 索引，
//     此处的校验主要在 service 层把 RequirementNumber 解析为 RequirementID）。
//   - 已 closed requirement：拒绝挂新 issue；已挂载 issue 仍可改其它字段，
//     但不能从游离/别处移过来。
//   - 已归档 project：所有写操作拒绝。
package project

import (
	"context"

	"github.com/google/uuid"

	"github.com/yan1h/agent-coding-workflow/internal/audit"
	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
)

type issueService struct {
	repo  Repository
	audit audit.Recorder
}

// NewIssueService 构造 IssueService。
func NewIssueService(repo Repository, rec audit.Recorder) IssueService {
	return &issueService{repo: repo, audit: rec}
}

// resolveRequirement 把 *number 解析为 *Requirement，并校验同 project + 未关闭。
// number == nil → 返回 (nil, nil) 表示不挂载。
func (s *issueService) resolveRequirement(ctx context.Context, p *Project, number *int) (*Requirement, error) {
	if number == nil {
		return nil, nil
	}
	r, err := s.repo.GetRequirementByNumber(ctx, p.ID, *number)
	if err != nil {
		return nil, err
	}
	if r.Status == StatusClosed {
		return nil, errs.New(errs.CodeRequirementClosed, "目标需求已关闭，无法挂载")
	}
	return r, nil
}

func (s *issueService) Create(ctx context.Context, c Caller, slug string, in CreateIssueInput) (*Issue, error) {
	if in.Title == "" {
		return nil, errs.New(errs.CodeInvalidInput, "title 必填")
	}
	p, err := loadProjectForWrite(ctx, s.repo, c, slug)
	if err != nil {
		return nil, err
	}
	r, err := s.resolveRequirement(ctx, p, in.RequirementNumber)
	if err != nil {
		return nil, err
	}
	var reqID *uuid.UUID
	if r != nil {
		reqID = &r.ID
	}
	parentID, err := s.resolveParent(ctx, p, in.ParentNumber)
	if err != nil {
		return nil, err
	}
	prio, err := normalizePriority(in.Priority)
	if err != nil {
		return nil, err
	}
	in1 := &Issue{
		ID: uuid.New(), ProjectID: p.ID, RequirementID: reqID,
		WorkspaceID: in.WorkspaceID,
		Title:       in.Title, Description: in.Description,
		AssigneeID: in.AssigneeID, CreatedBy: c.UserID,
		ParentID: parentID, Priority: prio,
	}
	out, err := s.createWithRetry(ctx, in1)
	if err != nil {
		return nil, err
	}
	meta := map[string]any{"project_id": p.ID.String(), "number": out.Number}
	if reqID != nil {
		meta["requirement_id"] = reqID.String()
	}
	if in.WorkspaceID != nil {
		meta["workspace_id"] = in.WorkspaceID.String()
	}
	if parentID != nil {
		meta["parent_id"] = parentID.String()
	}
	s.recordAudit(ctx, c, "issue.create", out.ID.String(), meta)
	return out, nil
}

// resolveParent 把 *number 解析为父 issue 的 *id，并校验同 project。number == nil →
// (nil, nil)。父 issue 不存在 → NotFound。
func (s *issueService) resolveParent(ctx context.Context, p *Project, number *int) (*uuid.UUID, error) {
	if number == nil {
		return nil, nil
	}
	parent, err := s.repo.GetIssueByNumber(ctx, p.ID, *number)
	if err != nil {
		return nil, err
	}
	id := parent.ID
	return &id, nil
}

// normalizePriority 校验 priority 在 0..3，nil → 0（默认）。
func normalizePriority(p *int) (int, error) {
	if p == nil {
		return 0, nil
	}
	if *p < 0 || *p > 3 {
		return 0, errs.New(errs.CodeInvalidInput, "priority 必须在 0..3")
	}
	return *p, nil
}

func (s *issueService) createWithRetry(ctx context.Context, in *Issue) (*Issue, error) {
	for attempt := 0; attempt < 2; attempt++ {
		out, err := s.repo.CreateIssue(ctx, in)
		if err == nil {
			return out, nil
		}
		if !IsUniqueViolation(err) {
			return nil, err
		}
	}
	return nil, errs.New(errs.CodeInternal, "issue number 持续冲突")
}

func (s *issueService) Get(ctx context.Context, c Caller, slug string, number int) (*Issue, error) {
	p, err := loadProjectForRead(ctx, s.repo, c, slug)
	if err != nil {
		return nil, err
	}
	return s.repo.GetIssueByNumber(ctx, p.ID, number)
}

func (s *issueService) List(ctx context.Context, c Caller, slug string, f IssueFilter) ([]*Issue, error) {
	p, err := loadProjectForRead(ctx, s.repo, c, slug)
	if err != nil {
		return nil, err
	}
	if f.Status != "" && f.Status != StatusOpen && f.Status != StatusClosed {
		return nil, errs.New(errs.CodeInvalidInput, "status 非法")
	}
	// f.Requirement 三态：nil / *==0 (none) / *>0 (number → id)
	var reqID *uuid.UUID
	if f.Requirement != nil && *f.Requirement > 0 {
		r, err := s.repo.GetRequirementByNumber(ctx, p.ID, *f.Requirement)
		if err != nil {
			return nil, err
		}
		reqID = &r.ID
	}
	return s.repo.ListIssues(ctx, p.ID, f, reqID)
}

func (s *issueService) Update(ctx context.Context, c Caller, slug string, number int, in UpdateIssueInput) (*Issue, error) {
	p, err := loadProjectForWrite(ctx, s.repo, c, slug)
	if err != nil {
		return nil, err
	}
	cur, err := s.repo.GetIssueByNumber(ctx, p.ID, number)
	if err != nil {
		return nil, err
	}

	title, desc := cur.Title, cur.Description
	changed := map[string]any{}
	if in.Title != nil && *in.Title != cur.Title {
		title = *in.Title
		changed["title"] = "<changed>"
	}
	if in.Description != nil && *in.Description != cur.Description {
		desc = *in.Description
		changed["description"] = "<changed>"
	}

	// requirement 三态：未传（in.RequirementNumber == nil）→ 保持
	//                 传 nil 指针（**nil）→ detach
	//                 传值（*RequirementNumber 不是 nil）→ resolve & validate
	reqID := cur.RequirementID
	if in.RequirementNumber != nil {
		auditFrom := "<none>"
		if cur.RequirementID != nil {
			auditFrom = cur.RequirementID.String()
		}
		var auditTo string
		if *in.RequirementNumber == nil {
			reqID = nil
			auditTo = "<none>"
		} else {
			r, err := s.resolveRequirement(ctx, p, *in.RequirementNumber)
			if err != nil {
				return nil, err
			}
			reqID = &r.ID
			auditTo = r.ID.String()
		}
		if auditFrom != auditTo {
			changed["from_requirement"] = auditFrom
			changed["to_requirement"] = auditTo
		}
	}

	// workspace 三态：nil（未提供）/ &uuid.Nil（解绑）/ &id（关联）
	nextWS := cur.WorkspaceID
	if in.WorkspaceID != nil {
		if *in.WorkspaceID == uuid.Nil {
			nextWS = nil
		} else {
			v := *in.WorkspaceID
			nextWS = &v
		}
		auditFrom := "<none>"
		if cur.WorkspaceID != nil {
			auditFrom = cur.WorkspaceID.String()
		}
		auditTo := "<none>"
		if nextWS != nil {
			auditTo = nextWS.String()
		}
		if auditFrom != auditTo {
			changed["from_workspace"] = auditFrom
			changed["to_workspace"] = auditTo
		}
	}

	if len(changed) == 0 {
		return cur, nil
	}

	out, err := s.repo.UpdateIssueCore(ctx, cur.ID, title, desc, reqID, nextWS)
	if err != nil {
		return nil, err
	}
	s.recordAudit(ctx, c, "issue.update", out.ID.String(), map[string]any{"changed_fields": changed})
	return out, nil
}

func (s *issueService) Assign(ctx context.Context, c Caller, slug string, number int, assignee *uuid.UUID) (*Issue, error) {
	p, err := loadProjectForWrite(ctx, s.repo, c, slug)
	if err != nil {
		return nil, err
	}
	cur, err := s.repo.GetIssueByNumber(ctx, p.ID, number)
	if err != nil {
		return nil, err
	}
	from := "<none>"
	if cur.AssigneeID != nil {
		from = cur.AssigneeID.String()
	}
	to := "<none>"
	if assignee != nil {
		to = assignee.String()
	}
	out, err := s.repo.UpdateIssueAssignee(ctx, cur.ID, assignee)
	if err != nil {
		return nil, err
	}
	s.recordAudit(ctx, c, "issue.assign", out.ID.String(), map[string]any{"from": from, "to": to})
	return out, nil
}

func (s *issueService) Close(ctx context.Context, c Caller, slug string, number int) error {
	p, err := loadProjectForWrite(ctx, s.repo, c, slug)
	if err != nil {
		return err
	}
	cur, err := s.repo.GetIssueByNumber(ctx, p.ID, number)
	if err != nil {
		return err
	}
	if cur.Status == StatusClosed {
		return nil
	}
	if err := s.repo.CloseIssue(ctx, cur.ID); err != nil {
		return err
	}
	s.recordAudit(ctx, c, "issue.close", cur.ID.String(), nil)
	return nil
}

func (s *issueService) Reopen(ctx context.Context, c Caller, slug string, number int) error {
	p, err := loadProjectForWrite(ctx, s.repo, c, slug)
	if err != nil {
		return err
	}
	cur, err := s.repo.GetIssueByNumber(ctx, p.ID, number)
	if err != nil {
		return err
	}
	if cur.Status == StatusOpen {
		return nil
	}
	if err := s.repo.ReopenIssue(ctx, cur.ID); err != nil {
		return err
	}
	s.recordAudit(ctx, c, "issue.reopen", cur.ID.String(), nil)
	return nil
}

// ===== 任务分解 / 依赖图（autonomy roadmap §10）=====

// CreateSubtask 在 parentNumber 指向的 issue 下创建子任务。复用 Create 的校验/重试/审计，
// 把 parentNumber 注入 CreateIssueInput.ParentNumber。
func (s *issueService) CreateSubtask(ctx context.Context, c Caller, slug string, parentNumber int, in CreateIssueInput) (*Issue, error) {
	pn := parentNumber
	in.ParentNumber = &pn
	out, err := s.Create(ctx, c, slug, in)
	if err != nil {
		return nil, err
	}
	s.recordAudit(ctx, c, "issue.create_subtask", out.ID.String(),
		map[string]any{"parent_number": parentNumber, "number": out.Number})
	return out, nil
}

// AddDependency 新增依赖边：blocked 被 blocker 阻塞。拒绝自依赖、跨 project、重复、成环。
func (s *issueService) AddDependency(ctx context.Context, c Caller, slug string, in AddDependencyInput) (*Dependency, error) {
	if in.BlockedNumber == in.BlockerNumber {
		return nil, errs.New(errs.CodeInvalidInput, "issue 不能依赖自身")
	}
	p, err := loadProjectForWrite(ctx, s.repo, c, slug)
	if err != nil {
		return nil, err
	}
	blocked, err := s.repo.GetIssueByNumber(ctx, p.ID, in.BlockedNumber)
	if err != nil {
		return nil, err
	}
	blocker, err := s.repo.GetIssueByNumber(ctx, p.ID, in.BlockerNumber)
	if err != nil {
		return nil, err
	}
	// 环检查：新增 "blocked blocked-by blocker" 成环，当且仅当 blocked 已能沿 blocked-by
	// 边到达 blocker（即 blocker 已（传递地）依赖 blocked）。从 blocker 出发遍历其
	// blocker 集合，若到达 blocked 则成环。
	cyclic, err := s.reachable(ctx, blocker.ID, blocked.ID)
	if err != nil {
		return nil, err
	}
	if cyclic {
		return nil, errs.New(errs.CodeInvalidInput, "新增依赖会形成环")
	}
	dep := &Dependency{
		ID: uuid.New(), ProjectID: p.ID,
		BlockedID: blocked.ID, BlockerID: blocker.ID, CreatedBy: c.UserID,
	}
	out, err := s.repo.CreateDependency(ctx, dep)
	if err != nil {
		return nil, err
	}
	s.recordAudit(ctx, c, "issue.add_dependency", out.ID.String(),
		map[string]any{"blocked_number": in.BlockedNumber, "blocker_number": in.BlockerNumber})
	return out, nil
}

// reachable 报告从 start 出发沿 blocked-by 边（start 的 blocker、blocker 的 blocker …）
// 是否能到达 target。迭代 BFS + visited 防环死循环。
func (s *issueService) reachable(ctx context.Context, start, target uuid.UUID) (bool, error) {
	if start == target {
		return true, nil
	}
	visited := map[uuid.UUID]bool{start: true}
	queue := []uuid.UUID{start}
	for len(queue) > 0 {
		cur := queue[0]
		queue = queue[1:]
		blockers, err := s.repo.ListBlockerIDs(ctx, cur)
		if err != nil {
			return false, err
		}
		for _, b := range blockers {
			if b == target {
				return true, nil
			}
			if !visited[b] {
				visited[b] = true
				queue = append(queue, b)
			}
		}
	}
	return false, nil
}

// RemoveDependency 删除一条依赖边。边不存在返回 NotFound。
func (s *issueService) RemoveDependency(ctx context.Context, c Caller, slug string, blockedNumber, blockerNumber int) error {
	p, err := loadProjectForWrite(ctx, s.repo, c, slug)
	if err != nil {
		return err
	}
	blocked, err := s.repo.GetIssueByNumber(ctx, p.ID, blockedNumber)
	if err != nil {
		return err
	}
	blocker, err := s.repo.GetIssueByNumber(ctx, p.ID, blockerNumber)
	if err != nil {
		return err
	}
	n, err := s.repo.DeleteDependency(ctx, p.ID, blocked.ID, blocker.ID)
	if err != nil {
		return err
	}
	if n == 0 {
		return errs.New(errs.CodeNotFound, "依赖不存在")
	}
	s.recordAudit(ctx, c, "issue.remove_dependency", blocked.ID.String(),
		map[string]any{"blocked_number": blockedNumber, "blocker_number": blockerNumber})
	return nil
}

// ListDependencies 返回 (blockedBy: 阻塞本 issue 的 issues, blocks: 被本 issue 阻塞的 issues)。
func (s *issueService) ListDependencies(ctx context.Context, c Caller, slug string, number int) ([]*Issue, []*Issue, error) {
	p, err := loadProjectForRead(ctx, s.repo, c, slug)
	if err != nil {
		return nil, nil, err
	}
	iss, err := s.repo.GetIssueByNumber(ctx, p.ID, number)
	if err != nil {
		return nil, nil, err
	}
	blockedBy, err := s.repo.ListBlockers(ctx, iss.ID)
	if err != nil {
		return nil, nil, err
	}
	blocks, err := s.repo.ListBlocked(ctx, iss.ID)
	if err != nil {
		return nil, nil, err
	}
	return blockedBy, blocks, nil
}

func (s *issueService) recordAudit(ctx context.Context, c Caller, action, targetID string, meta map[string]any) {
	if s.audit == nil {
		return
	}
	uid := c.UserID
	_ = s.audit.Record(context.WithoutCancel(ctx), audit.Entry{
		UserID: &uid, Action: action, TargetType: "issue", TargetID: targetID, Metadata: meta,
	})
}
