// service.go implements the change-request application service, including the
// merge gate (Service.Merge). Auth delegates to ProjectAccess (owner+admin
// write), mirroring workspace's narrow-interface approach so this package does
// not depend on internal/project wholesale.
package changerequest

import (
	"context"
	"time"

	"github.com/google/uuid"

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

// WorkspaceLookup resolves a workspace's project + remote URL + default branch
// for RepoRef derivation and target-branch defaulting. Narrow interface over
// workspace.Repository so we avoid importing the full workspace service.
type WorkspaceLookup interface {
	GetWorkspaceMeta(ctx context.Context, wsID uuid.UUID) (WorkspaceMeta, error)
}

// WorkspaceMeta is the workspace projection the change-request service needs.
type WorkspaceMeta struct {
	ProjectID     uuid.UUID
	GitRemoteURL  string
	DefaultBranch string
}

// ProjectAccess is the auth interface (same shape as workspace.ProjectAccess):
// ResolveByID returns (canRead, canWrite, err) for a project.
type ProjectAccess interface {
	ResolveByID(ctx context.Context, callerID uuid.UUID, isAdmin bool, projectID uuid.UUID) (bool, bool, error)
}

// Config controls the merge gate policy.
type Config struct {
	RequireCIPass bool   // when true, merge also requires ci_state=='success'
	MergeMethod   string // default host merge method ("merge"|"squash"|"rebase"); "merge" when empty
	Host          string // bare host the provider serves, for RepoRef host check; empty => skip
}

type service struct {
	repo     Repository
	ws       WorkspaceLookup
	rec      audit.Recorder
	provider vcs.Provider
	pa       ProjectAccess
	cfg      Config
}

// NewService constructs the change-request Service.
func NewService(repo Repository, ws WorkspaceLookup, rec audit.Recorder, provider vcs.Provider, pa ProjectAccess, cfg Config) Service {
	if cfg.MergeMethod == "" {
		cfg.MergeMethod = "merge"
	}
	return &service{repo: repo, ws: ws, rec: rec, provider: provider, pa: pa, cfg: cfg}
}

// Create opens a host PR and persists the change_requests row.
func (s *service) Create(ctx context.Context, c Caller, wsID uuid.UUID, in CreateInput) (*ChangeRequest, error) {
	meta, err := s.ws.GetWorkspaceMeta(ctx, wsID)
	if err != nil {
		return nil, err
	}
	if err := s.guardWrite(ctx, c, meta.ProjectID); err != nil {
		return nil, err
	}
	if in.SourceBranch == "" {
		return nil, errs.New(errs.CodeInvalidInput, "source_branch required")
	}
	if in.Title == "" {
		return nil, errs.New(errs.CodeInvalidInput, "title required")
	}
	target := in.TargetBranch
	if target == "" {
		target = meta.DefaultBranch
	}
	if target == "" {
		target = "main"
	}
	if in.SourceBranch == target {
		return nil, errs.New(errs.CodeInvalidInput, "source_branch and target_branch must differ")
	}

	repoRef, err := vcs.ParseRepoRef(meta.GitRemoteURL, s.cfg.Host)
	if err != nil {
		return nil, err
	}

	pr, err := s.provider.CreatePR(ctx, vcs.CreatePRInput{
		Repo: repoRef, Head: in.SourceBranch, Base: target,
		Title: in.Title, Body: in.Description,
	})
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeUpstream, "open pull request")
	}

	cr := &ChangeRequest{
		ID:            uuid.New(),
		ProjectID:     meta.ProjectID,
		WorkspaceID:   wsID,
		RequirementID: in.RequirementID,
		IssueID:       in.IssueID,
		Title:         in.Title,
		Description:   in.Description,
		SourceBranch:  in.SourceBranch,
		TargetBranch:  target,
		CIState:       CIState(pr.CIState),
		Provider:      "gitea",
		ExternalURL:   pr.HTMLURL,
		CreatedBy:     c.UserID,
	}
	if cr.CIState == "" {
		cr.CIState = CIUnknown
	}
	extID := pr.Number
	cr.ExternalID = &extID

	var created *ChangeRequest
	err = s.repo.InTx(ctx, func(tx Tx) error {
		n, nerr := tx.NextNumber(ctx, meta.ProjectID)
		if nerr != nil {
			return nerr
		}
		cr.Number = n
		c2, cerr := tx.Create(ctx, cr)
		if cerr != nil {
			return cerr
		}
		created = c2
		return nil
	})
	if err != nil {
		return nil, err
	}

	s.audit(ctx, &c.UserID, "change_request.created", created.ID.String(), map[string]any{
		"number": created.Number, "external_id": extID, "source": in.SourceBranch, "target": target,
	})
	return created, nil
}

func (s *service) Get(ctx context.Context, c Caller, id uuid.UUID) (*ChangeRequest, error) {
	cr, err := s.repo.GetByID(ctx, id)
	if err != nil {
		return nil, err
	}
	if err := s.guardRead(ctx, c, cr.ProjectID); err != nil {
		return nil, err
	}
	return cr, nil
}

func (s *service) GetByNumber(ctx context.Context, c Caller, projectSlug string, number int) (*ChangeRequest, error) {
	cr, err := s.repo.GetByNumber(ctx, projectSlug, number)
	if err != nil {
		return nil, err
	}
	if err := s.guardRead(ctx, c, cr.ProjectID); err != nil {
		return nil, err
	}
	return cr, nil
}

func (s *service) ListByWorkspace(ctx context.Context, c Caller, wsID uuid.UUID) ([]*ChangeRequest, error) {
	meta, err := s.ws.GetWorkspaceMeta(ctx, wsID)
	if err != nil {
		return nil, err
	}
	if err := s.guardRead(ctx, c, meta.ProjectID); err != nil {
		return nil, err
	}
	return s.repo.ListByWorkspace(ctx, wsID)
}

// Review records a human verdict. Owner/admin only. Not an MCP tool.
func (s *service) Review(ctx context.Context, c Caller, id uuid.UUID, verdict Verdict, note string) (*ChangeRequest, error) {
	if !verdict.IsValid() || verdict == VerdictPending {
		return nil, errs.New(errs.CodeInvalidInput, "invalid review verdict")
	}
	cr, err := s.repo.GetByID(ctx, id)
	if err != nil {
		return nil, err
	}
	if err := s.guardWrite(ctx, c, cr.ProjectID); err != nil {
		return nil, err
	}
	if cr.State != StateOpen {
		return nil, errs.New(errs.CodeFailedPrecondition, "cannot review a non-open change request")
	}
	updated, err := s.repo.UpdateReview(ctx, id, verdict, c.UserID)
	if err != nil {
		return nil, err
	}
	if note != "" && updated.ExternalID != nil {
		if repoRef, perr := s.repoRefFor(ctx, updated.WorkspaceID); perr == nil {
			_ = s.provider.Comment(ctx, repoRef, *updated.ExternalID, "Review ("+string(verdict)+"): "+note)
		}
	}
	s.audit(ctx, &c.UserID, "change_request.reviewed", id.String(), map[string]any{
		"verdict": string(verdict),
	})
	return updated, nil
}

// Merge is the gate. It requires review_verdict=='approved' (and ci success
// when RequireCIPass and not admin), then merges the host PR.
func (s *service) Merge(ctx context.Context, c Caller, id uuid.UUID, method string) (*ChangeRequest, error) {
	cr, err := s.repo.GetByID(ctx, id)
	if err != nil {
		return nil, err
	}
	if err := s.guardWrite(ctx, c, cr.ProjectID); err != nil {
		return nil, err
	}
	if cr.State != StateOpen {
		return nil, errs.New(errs.CodeFailedPrecondition, "change request is not open")
	}
	// THE GATE: review must be approved.
	if cr.ReviewVerdict != VerdictApproved {
		return nil, errs.New(errs.CodeFailedPrecondition, "merge blocked: review not approved")
	}
	// Optional CI gate; admins may override.
	if s.cfg.RequireCIPass && !c.IsAdmin && cr.CIState != CISuccess {
		return nil, errs.New(errs.CodeFailedPrecondition, "merge blocked: CI not passing")
	}
	if cr.ExternalID == nil {
		return nil, errs.New(errs.CodeFailedPrecondition, "change request has no external PR")
	}

	repoRef, err := s.repoRefFor(ctx, cr.WorkspaceID)
	if err != nil {
		return nil, err
	}
	mm := method
	if mm == "" {
		mm = s.cfg.MergeMethod
	}
	mergeSHA, err := s.provider.Merge(ctx, repoRef, *cr.ExternalID, vcs.MergeInput{Method: mm})
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeFailedPrecondition, "host merge failed")
	}

	now := time.Now()
	updated, err := s.repo.UpdateState(ctx, id, StateMerged, mergeSHA, &now)
	if err != nil {
		return nil, err
	}
	s.audit(ctx, &c.UserID, "change_request.merged", id.String(), map[string]any{
		"method": mm, "merge_sha": mergeSHA,
	})
	return updated, nil
}

// SyncStatus refreshes ci_state/state from the host PR.
func (s *service) SyncStatus(ctx context.Context, c Caller, id uuid.UUID) (*ChangeRequest, error) {
	cr, err := s.repo.GetByID(ctx, id)
	if err != nil {
		return nil, err
	}
	if err := s.guardRead(ctx, c, cr.ProjectID); err != nil {
		return nil, err
	}
	if cr.ExternalID == nil {
		return cr, nil
	}
	repoRef, err := s.repoRefFor(ctx, cr.WorkspaceID)
	if err != nil {
		return nil, err
	}
	pr, err := s.provider.GetPR(ctx, repoRef, *cr.ExternalID)
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeUpstream, "fetch pull request status")
	}
	newState := cr.State
	switch {
	case pr.Merged:
		newState = StateMerged
	case pr.State == "closed":
		newState = StateClosed
	default:
		newState = StateOpen
	}
	ci := CIState(pr.CIState)
	if !validCI(ci) {
		ci = CIUnknown
	}
	return s.repo.UpdateCI(ctx, id, ci, newState)
}

// ===== helpers =====

func (s *service) repoRefFor(ctx context.Context, wsID uuid.UUID) (vcs.RepoRef, error) {
	meta, err := s.ws.GetWorkspaceMeta(ctx, wsID)
	if err != nil {
		return vcs.RepoRef{}, err
	}
	return vcs.ParseRepoRef(meta.GitRemoteURL, s.cfg.Host)
}

func (s *service) guardRead(ctx context.Context, c Caller, projectID uuid.UUID) error {
	canRead, _, err := s.pa.ResolveByID(ctx, c.UserID, c.IsAdmin, projectID)
	if err != nil {
		return err
	}
	if !canRead {
		return errs.New(errs.CodeForbidden, "no read access")
	}
	return nil
}

func (s *service) guardWrite(ctx context.Context, c Caller, projectID uuid.UUID) error {
	_, canWrite, err := s.pa.ResolveByID(ctx, c.UserID, c.IsAdmin, projectID)
	if err != nil {
		return err
	}
	if !canWrite {
		return errs.New(errs.CodeForbidden, "no write access")
	}
	return nil
}

func (s *service) audit(ctx context.Context, uid *uuid.UUID, action, id string, meta map[string]any) {
	_ = s.rec.Record(ctx, audit.Entry{
		UserID: uid, Action: action, TargetType: "change_request", TargetID: id, Metadata: meta,
	})
}

func validCI(ci CIState) bool {
	switch ci {
	case CIUnknown, CIPending, CISuccess, CIFailure:
		return true
	}
	return false
}
