// Package project 内的 requirement_service.go 实装 RequirementService。
//
// 设计取舍：
//   - phase 不强制单向，但必须在 AllPhases 枚举内。
//   - 已 closed requirement：禁止 phase 切换 + 禁止挂新 issue（后者由
//     IssueService 实施）；现有挂载 issue 仍可正常修改。
//   - 已归档 project 下任何写操作直接 CodeProjectArchived。
//   - number 自增由 SQL 子查询 + UNIQUE 兜底；service 在 Create 上捕获
//     unique_violation 重试 1 次，再次失败转 CodeInternal。
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 requirementService struct {
	repo    Repository
	audit   audit.Recorder
	emitter PhaseEventEmitter
	gate    GateRunner
}

// NewRequirementService 构造 RequirementService。
//
// emitter 为 nil 时不发 requirement.phase_change 领域事件（仅审计），gate 为 nil
// 时保持 any-to-any 旧行为（不做 entry 网关校验）——二者均允许测试/精简场景注入 nil。
func NewRequirementService(repo Repository, rec audit.Recorder, emitter PhaseEventEmitter, gate GateRunner) RequirementService {
	return &requirementService{repo: repo, audit: rec, emitter: emitter, gate: gate}
}

// loadProjectForWrite 解析 slug → 校验 caller 写权限 → 校验未归档。
// 成员/角色 ACL：member/admin 角色叠加写授权（见 assertWritable）。
func loadProjectForWrite(ctx context.Context, repo Repository, c Caller, slug string) (*Project, error) {
	p, err := repo.GetProjectBySlug(ctx, slug)
	if err != nil {
		return nil, err
	}
	if err := assertWritable(p, c, callerRole(ctx, repo, p, c)); err != nil {
		return nil, err
	}
	return p, nil
}

func loadProjectForRead(ctx context.Context, repo Repository, c Caller, slug string) (*Project, error) {
	p, err := repo.GetProjectBySlug(ctx, slug)
	if err != nil {
		return nil, err
	}
	if err := assertReadable(p, c, callerRole(ctx, repo, p, c)); err != nil {
		return nil, err
	}
	return p, nil
}

// callerRole 是 loadProjectFor* 的成员角色查询 helper（不在 projectService 上，
// 故走 repo）。owner/global-admin 隐式 admin；查询失败按 RoleNone（既有规则兜底）。
func callerRole(ctx context.Context, repo Repository, p *Project, c Caller) Role {
	if c.IsAdmin || p.OwnerID == c.UserID {
		return RoleAdmin
	}
	role, err := repo.GetMemberRole(ctx, p.ID, c.UserID)
	if err != nil {
		return RoleNone
	}
	return role
}

func (s *requirementService) Create(ctx context.Context, c Caller, slug string, in CreateRequirementInput) (*Requirement, 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
	}
	owner := c.UserID
	if in.OwnerID != nil {
		owner = *in.OwnerID
	}
	in1 := &Requirement{
		ID: uuid.New(), ProjectID: p.ID,
		WorkspaceID: in.WorkspaceID,
		Title:       in.Title, Description: in.Description,
		OwnerID: owner,
	}
	out, err := s.createWithRetry(ctx, in1)
	if err != nil {
		return nil, err
	}
	meta := map[string]any{
		"project_id": p.ID.String(), "number": out.Number, "title": out.Title,
	}
	if in.WorkspaceID != nil {
		meta["workspace_id"] = in.WorkspaceID.String()
	}
	s.recordAudit(ctx, c, "requirement.create", out.ID.String(), meta)
	return out, nil
}

// createWithRetry 把 number 竞争吞下并重试 1 次；再次失败转 CodeInternal。
func (s *requirementService) createWithRetry(ctx context.Context, in *Requirement) (*Requirement, error) {
	for attempt := 0; attempt < 2; attempt++ {
		out, err := s.repo.CreateRequirement(ctx, in)
		if err == nil {
			return out, nil
		}
		if !IsUniqueViolation(err) {
			return nil, err
		}
		// number 撞了，下一轮 SQL 子查询会拿到新 max
	}
	return nil, errs.New(errs.CodeInternal, "requirement number 持续冲突")
}

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

func (s *requirementService) List(ctx context.Context, c Caller, slug string, f RequirementFilter) ([]*Requirement, error) {
	p, err := loadProjectForRead(ctx, s.repo, c, slug)
	if err != nil {
		return nil, err
	}
	if f.Phase != "" && !f.Phase.IsValid() {
		return nil, errs.New(errs.CodeInvalidInput, "phase 非法")
	}
	if f.Status != "" && f.Status != StatusOpen && f.Status != StatusClosed {
		return nil, errs.New(errs.CodeInvalidInput, "status 非法")
	}
	return s.repo.ListRequirements(ctx, p.ID, f.Phase, f.Status)
}

func (s *requirementService) Update(ctx context.Context, c Caller, slug string, number int, in UpdateRequirementInput) (*Requirement, error) {
	p, err := loadProjectForWrite(ctx, s.repo, c, slug)
	if err != nil {
		return nil, err
	}
	r, err := s.repo.GetRequirementByNumber(ctx, p.ID, number)
	if err != nil {
		return nil, err
	}
	changed := map[string]any{}
	if in.Title != nil {
		r.Title = *in.Title
		changed["title"] = "<changed>"
	}
	if in.Description != nil {
		r.Description = *in.Description
		changed["description"] = "<changed>"
	}
	if in.OwnerID != nil {
		r.OwnerID = *in.OwnerID
		changed["owner_id"] = in.OwnerID.String()
	}
	// workspace 三态：nil（未提供）/ &uuid.Nil（解绑）/ &id（关联）
	if in.WorkspaceID != nil {
		auditFrom := "<none>"
		if r.WorkspaceID != nil {
			auditFrom = r.WorkspaceID.String()
		}
		if *in.WorkspaceID == uuid.Nil {
			r.WorkspaceID = nil
		} else {
			v := *in.WorkspaceID
			r.WorkspaceID = &v
		}
		auditTo := "<none>"
		if r.WorkspaceID != nil {
			auditTo = r.WorkspaceID.String()
		}
		if auditFrom != auditTo {
			changed["from_workspace"] = auditFrom
			changed["to_workspace"] = auditTo
		}
	}
	if len(changed) == 0 {
		return r, nil
	}
	out, err := s.repo.UpdateRequirement(ctx, r)
	if err != nil {
		return nil, err
	}
	s.recordAudit(ctx, c, "requirement.update", out.ID.String(), map[string]any{"changed_fields": changed})
	return out, nil
}

func (s *requirementService) ChangePhase(ctx context.Context, c Caller, slug string, number int, to Phase) (*Requirement, error) {
	if !to.IsValid() {
		return nil, errs.New(errs.CodeInvalidInput, "phase 非法")
	}
	p, err := loadProjectForWrite(ctx, s.repo, c, slug)
	if err != nil {
		return nil, err
	}
	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, "已关闭需求禁止 phase 切换")
	}
	if r.Phase == to {
		return r, nil
	}
	from := r.Phase
	// entry 网关：目标 phase 声明的全部前置谓词必须通过。失败返回 CodePhaseGateFailed，
	// 且不写库、不发事件。gate 为 nil 时跳过（保持 any-to-any）。
	if s.gate != nil {
		if err := s.gate.Check(ctx, r, from, to); err != nil {
			return nil, err
		}
	}
	out, err := s.repo.UpdateRequirementPhase(ctx, r.ID, to)
	if err != nil {
		return nil, err
	}
	s.recordAudit(ctx, c, "requirement.phase_change", out.ID.String(), map[string]any{
		"from": string(from), "to": string(to),
	})
	// 领域事件（与审计互补）：fire-and-forget，投递由 emitter 异步处理，失败不阻塞流转。
	if s.emitter != nil {
		s.emitter.EmitPhaseChange(context.WithoutCancel(ctx), PhaseChangeEvent{
			ProjectID:         p.ID,
			ProjectSlug:       p.Slug,
			RequirementID:     out.ID,
			RequirementNumber: out.Number,
			From:              from,
			To:                to,
			ActorID:           c.UserID,
			OwnerID:           out.OwnerID,
		})
	}
	return out, nil
}

func (s *requirementService) Close(ctx context.Context, c Caller, slug string, number int) error {
	p, err := loadProjectForWrite(ctx, s.repo, c, slug)
	if err != nil {
		return err
	}
	r, err := s.repo.GetRequirementByNumber(ctx, p.ID, number)
	if err != nil {
		return err
	}
	if r.Status == StatusClosed {
		return nil
	}
	if err := s.repo.CloseRequirement(ctx, r.ID); err != nil {
		return err
	}
	s.recordAudit(ctx, c, "requirement.close", r.ID.String(), nil)
	return nil
}

func (s *requirementService) Reopen(ctx context.Context, c Caller, slug string, number int) error {
	p, err := loadProjectForWrite(ctx, s.repo, c, slug)
	if err != nil {
		return err
	}
	r, err := s.repo.GetRequirementByNumber(ctx, p.ID, number)
	if err != nil {
		return err
	}
	if r.Status == StatusOpen {
		return nil
	}
	if err := s.repo.ReopenRequirement(ctx, r.ID); err != nil {
		return err
	}
	s.recordAudit(ctx, c, "requirement.reopen", r.ID.String(), nil)
	return nil
}

func (s *requirementService) 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: "requirement", TargetID: targetID, Metadata: meta,
	})
}
