// Package project 内的 project_service.go 实装 ProjectService 接口。
//
// 鉴权矩阵（PM spec §1.3）：
//
//	read   private:  owner + admin
//	read   internal: 任意已登录
//	write  *:        owner + admin
//
// 已归档项目下的所有写操作（包含 Requirement / Issue 的写）一律返回
// CodeProjectArchived；这是 spec 强约定，不区分 owner / admin。
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 projectService struct {
	repo  Repository
	audit audit.Recorder
}

// NewProjectService 用 Repository 与可选 audit recorder 构造 ProjectService。
// recorder == nil 时跳过审计写入（便于单测）。
func NewProjectService(repo Repository, rec audit.Recorder) ProjectService {
	return &projectService{repo: repo, audit: rec}
}

// assertReadable 是 read 类操作的鉴权门面。成员/角色 ACL（autonomy roadmap §11）
// 在既有规则之上"叠加"成员授权（只增不减）：
//   - internal 可见性：任意登录用户可读（既有）
//   - owner / global-admin：可读（既有）
//   - 任何成员角色（viewer/member/admin）：可读（新增叠加）
//
// 失败统一返回 CodeNotFound（避免存在性探测）。role 由 service 在调用前查得，
// 非成员传 RoleNone。
func assertReadable(p *Project, c Caller, role Role) error {
	if p.Visibility == VisibilityInternal {
		return nil
	}
	if c.IsAdmin || p.OwnerID == c.UserID {
		return nil
	}
	if role.CanRead() {
		return nil
	}
	return errs.New(errs.CodeNotFound, "project 不存在")
}

// assertWritable 是 write 类操作的鉴权门面。成员/角色 ACL 叠加写授权：
//   - owner / global-admin：可写（既有）
//   - member / admin 成员角色：可写（新增叠加）
//   - viewer / 非成员：不可写
//
// 已归档项目额外拒绝；调用方应在执行写入前调用此函数。role 由 service 查得。
func assertWritable(p *Project, c Caller, role Role) error {
	if !c.IsAdmin && p.OwnerID != c.UserID && !role.CanWrite() {
		// stranger 对 private 看不见；对 internal/viewer 也不能写。统一 not_found
		// 防探测——但若 caller 至少能读（internal 可见性或 viewer 成员），暴露
		// forbidden 比 not_found 更准确（其存在性本就可见）。
		if p.Visibility == VisibilityPrivate && !role.CanRead() {
			return errs.New(errs.CodeNotFound, "project 不存在")
		}
		return errs.New(errs.CodeForbidden, "无权修改该项目")
	}
	if p.ArchivedAt != nil {
		return errs.New(errs.CodeProjectArchived, "项目已归档，禁止写操作")
	}
	return nil
}

// memberRoleFor 查 caller 在 project 的成员角色。owner/global-admin 隐式视为
// RoleAdmin（与回填一致，避免对 owner 多查一次也无妨——这里仍走 DB 但 owner
// 经迁移已有 admin 行）。查询失败按 RoleNone 处理（best-effort，既有 owner/admin
// 规则仍兜底）。
func (s *projectService) memberRoleFor(ctx context.Context, p *Project, c Caller) Role {
	if c.IsAdmin || p.OwnerID == c.UserID {
		return RoleAdmin
	}
	role, err := s.repo.GetMemberRole(ctx, p.ID, c.UserID)
	if err != nil {
		return RoleNone
	}
	return role
}

func (s *projectService) Create(ctx context.Context, c Caller, in CreateProjectInput) (*Project, error) {
	if in.Slug == "" || in.Name == "" {
		return nil, errs.New(errs.CodeInvalidInput, "slug / name 必填")
	}
	if in.Visibility == "" {
		in.Visibility = VisibilityPrivate
	}
	if !in.Visibility.IsValid() {
		return nil, errs.New(errs.CodeInvalidInput, "visibility 非法")
	}
	p := &Project{
		ID: uuid.New(), Slug: in.Slug, Name: in.Name, Description: in.Description,
		Visibility: in.Visibility, OwnerID: c.UserID,
	}
	out, err := s.repo.CreateProject(ctx, p)
	if err != nil {
		return nil, err
	}
	s.recordAudit(ctx, c, "project.create", out.ID.String(), map[string]any{
		"slug": out.Slug, "name": out.Name, "visibility": string(out.Visibility),
	})
	return out, nil
}

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

// GetByID 与 Get 等价但按 uuid 寻址，用于 workspace.ProjectAccess.ResolveByID。
// 鉴权矩阵与 Get 相同：private 仅 owner+admin，internal 任意登录用户。
func (s *projectService) GetByID(ctx context.Context, c Caller, id uuid.UUID) (*Project, error) {
	p, err := s.repo.GetProjectByID(ctx, id)
	if err != nil {
		return nil, err
	}
	if err := assertReadable(p, c, s.memberRoleFor(ctx, p, c)); err != nil {
		return nil, err
	}
	return p, nil
}

func (s *projectService) List(ctx context.Context, c Caller, includeArchived bool) ([]*Project, error) {
	all, err := s.repo.ListProjects(ctx, includeArchived)
	if err != nil {
		return nil, err
	}
	out := make([]*Project, 0, len(all))
	for _, p := range all {
		// 列表场景：internal/owner/admin 命中即收，避免对每个 private 项目都查一次
		// 成员表；仅当上述既有规则不命中时才回退查成员角色（叠加 viewer/member）。
		if assertReadable(p, c, RoleNone) == nil {
			out = append(out, p)
			continue
		}
		if role := s.memberRoleFor(ctx, p, c); assertReadable(p, c, role) == nil {
			out = append(out, p)
		}
	}
	return out, nil
}

func (s *projectService) Update(ctx context.Context, c Caller, slug string, in UpdateProjectInput) (*Project, error) {
	p, err := s.repo.GetProjectBySlug(ctx, slug)
	if err != nil {
		return nil, err
	}
	if err := assertWritable(p, c, s.memberRoleFor(ctx, p, c)); err != nil {
		return nil, err
	}
	changed := map[string]any{}
	if in.Name != nil {
		p.Name = *in.Name
		changed["name"] = *in.Name
	}
	if in.Description != nil {
		p.Description = *in.Description
		changed["description"] = "<changed>"
	}
	if in.Visibility != nil {
		if !in.Visibility.IsValid() {
			return nil, errs.New(errs.CodeInvalidInput, "visibility 非法")
		}
		p.Visibility = *in.Visibility
		changed["visibility"] = string(*in.Visibility)
	}
	if len(changed) == 0 {
		return p, nil
	}
	out, err := s.repo.UpdateProject(ctx, p)
	if err != nil {
		return nil, err
	}
	s.recordAudit(ctx, c, "project.update", out.ID.String(), map[string]any{"changed_fields": changed})
	return out, nil
}

func (s *projectService) Archive(ctx context.Context, c Caller, slug string) error {
	p, err := s.repo.GetProjectBySlug(ctx, slug)
	if err != nil {
		return err
	}
	// 归档/取消归档要 owner / global-admin / project-admin 角色，但已归档不再额外
	// 阻塞 archive。project-admin 视为项目管理者，可执行生命周期操作。
	if !c.IsAdmin && p.OwnerID != c.UserID && !s.memberRoleFor(ctx, p, c).CanManageMembers() {
		if p.Visibility == VisibilityPrivate {
			return errs.New(errs.CodeNotFound, "project 不存在")
		}
		return errs.New(errs.CodeForbidden, "无权操作该项目")
	}
	if p.ArchivedAt != nil {
		return nil // 幂等
	}
	if err := s.repo.ArchiveProject(ctx, p.ID); err != nil {
		return err
	}
	s.recordAudit(ctx, c, "project.archive", p.ID.String(), nil)
	return nil
}

func (s *projectService) Unarchive(ctx context.Context, c Caller, slug string) error {
	p, err := s.repo.GetProjectBySlug(ctx, slug)
	if err != nil {
		return err
	}
	if !c.IsAdmin && p.OwnerID != c.UserID && !s.memberRoleFor(ctx, p, c).CanManageMembers() {
		if p.Visibility == VisibilityPrivate {
			return errs.New(errs.CodeNotFound, "project 不存在")
		}
		return errs.New(errs.CodeForbidden, "无权操作该项目")
	}
	if p.ArchivedAt == nil {
		return nil
	}
	if err := s.repo.UnarchiveProject(ctx, p.ID); err != nil {
		return err
	}
	s.recordAudit(ctx, c, "project.unarchive", p.ID.String(), nil)
	return nil
}

// ===== 成员/角色 ACL（autonomy roadmap §11）=====

// ListMembers 返回 slug 项目的全部成员。要求 caller 对项目可读（owner/admin/
// 任意成员/internal 可见性）。
func (s *projectService) ListMembers(ctx context.Context, c Caller, slug string) ([]*Member, error) {
	p, err := s.repo.GetProjectBySlug(ctx, slug)
	if err != nil {
		return nil, err
	}
	if err := assertReadable(p, c, s.memberRoleFor(ctx, p, c)); err != nil {
		return nil, err
	}
	return s.repo.ListMembers(ctx, p.ID)
}

// AddMember 新增或更新一名成员的角色。仅 project-admin 角色或 global-admin 可调用。
func (s *projectService) AddMember(ctx context.Context, c Caller, slug string, in AddMemberInput) (*Member, error) {
	if in.UserID == uuid.Nil {
		return nil, errs.New(errs.CodeInvalidInput, "user_id 必填")
	}
	if !in.Role.IsValid() {
		return nil, errs.New(errs.CodeInvalidInput, "role 非法（仅 viewer/member/admin）")
	}
	p, err := s.repo.GetProjectBySlug(ctx, slug)
	if err != nil {
		return nil, err
	}
	if err := s.assertCanManageMembers(ctx, p, c); err != nil {
		return nil, err
	}
	addedBy := c.UserID
	m, err := s.repo.UpsertMember(ctx, p.ID, in.UserID, in.Role, &addedBy)
	if err != nil {
		return nil, err
	}
	s.recordAudit(ctx, c, "project.member_add", p.ID.String(), map[string]any{
		"user_id": in.UserID.String(), "role": string(in.Role),
	})
	return m, nil
}

// RemoveMember 移除一名成员。仅 project-admin 角色或 global-admin 可调用。
// 拒绝移除项目 owner（owner 经回填为 admin，移除会让其失去隐式 owner 之外的角色行，
// 但 owner 仍由 OwnerID 兜底拥有权限——为避免误导，直接拒绝移除 owner）。
func (s *projectService) RemoveMember(ctx context.Context, c Caller, slug string, userID uuid.UUID) error {
	if userID == uuid.Nil {
		return errs.New(errs.CodeInvalidInput, "user_id 必填")
	}
	p, err := s.repo.GetProjectBySlug(ctx, slug)
	if err != nil {
		return err
	}
	if err := s.assertCanManageMembers(ctx, p, c); err != nil {
		return err
	}
	if userID == p.OwnerID {
		return errs.New(errs.CodeForbidden, "不能移除项目 owner")
	}
	n, err := s.repo.DeleteMember(ctx, p.ID, userID)
	if err != nil {
		return err
	}
	if n == 0 {
		return errs.New(errs.CodeNotFound, "成员不存在")
	}
	s.recordAudit(ctx, c, "project.member_remove", p.ID.String(), map[string]any{
		"user_id": userID.String(),
	})
	return nil
}

// MemberRole 返回 userID 在 projectID 的成员角色（非成员返回 RoleNone）。
// owner/global-admin 的隐式 admin 不在此体现——本方法只读 project_members 行；
// app 层 projectACL 自行叠加 owner/admin 判定。
func (s *projectService) MemberRole(ctx context.Context, projectID, userID uuid.UUID) (Role, error) {
	return s.repo.GetMemberRole(ctx, projectID, userID)
}

// assertCanManageMembers 校验 caller 可管理 p 的成员：global-admin、owner 或
// project-admin 角色。private 不可见者返回 NotFound 防探测。
func (s *projectService) assertCanManageMembers(ctx context.Context, p *Project, c Caller) error {
	if c.IsAdmin || p.OwnerID == c.UserID {
		return nil
	}
	role := s.memberRoleFor(ctx, p, c)
	if role.CanManageMembers() {
		return nil
	}
	// 不可读者：private 返回 NotFound 防探测；否则 Forbidden。
	if assertReadable(p, c, role) != nil {
		return errs.New(errs.CodeNotFound, "project 不存在")
	}
	return errs.New(errs.CodeForbidden, "无权管理项目成员")
}

// recordAudit 是 best-effort 写入：失败仅吞错（不影响业务）。ctx 用
// context.WithoutCancel 派生，使 HTTP 响应已返回但审计仍能落库。
func (s *projectService) 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: "project",
		TargetID:   targetID,
		Metadata:   meta,
	})
}
