package workspace

import (
	"context"

	"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/git"
)

// CredentialResolver 是 service 共享的解密回调，避免 GitOpsService 直接持有 *crypto.Encryptor。
type CredentialResolver func(ctx context.Context, wsID uuid.UUID) (*git.Credential, error)

// CommitSummaryHook is a best-effort callback fired after a successful commit to
// enqueue auto-doc/PR-summary generation. Injected from app.go via
// SetCommitSummaryHook to avoid a workspace→docs import cycle. May be nil.
type CommitSummaryHook func(ctx context.Context, wsID uuid.UUID, worktreeID *uuid.UUID, branch, commitSHA string) error

type gitOpsService struct {
	repo        Repository
	rec         audit.Recorder
	gitr        git.Runner
	pa          ProjectAccess
	cred        CredentialResolver
	summaryHook CommitSummaryHook
}

// NewGitOpsService 构造 GitOpsService。cred 由 WorkspaceService.loadDecryptedCredential 适配过来。
func NewGitOpsService(repo Repository, rec audit.Recorder, gitr git.Runner, pa ProjectAccess, cred CredentialResolver) GitOpsService {
	return &gitOpsService{repo: repo, rec: rec, gitr: gitr, pa: pa, cred: cred}
}

// SetCommitSummaryHook injects the auto-doc enqueue callback (app wiring).
func (s *gitOpsService) SetCommitSummaryHook(h CommitSummaryHook) { s.summaryHook = h }

// ===== Main =====

func (s *gitOpsService) StatusOnMain(ctx context.Context, c Caller, wsID uuid.UUID) ([]git.FileStatus, error) {
	ws, err := s.guardWorkspaceRead(ctx, c, wsID)
	if err != nil {
		return nil, err
	}
	st, err := s.gitr.Status(ctx, ws.MainPath)
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeGitCmdFailed, "git status")
	}
	return st, nil
}

func (s *gitOpsService) CommitOnMain(ctx context.Context, c Caller, wsID uuid.UUID, in CommitInput) (string, error) {
	ws, err := s.guardWorkspaceWrite(ctx, c, wsID)
	if err != nil {
		return "", err
	}
	sha, err := s.gitr.Commit(ctx, ws.MainPath, in.Message, in.AddAll)
	if err != nil {
		return "", errs.Wrap(err, errs.CodeGitCmdFailed, "git commit")
	}
	s.auditCommit(ctx, &c.UserID, wsID.String(), "main", in.Message, sha)
	s.fireCommitSummary(ctx, &c.UserID, wsID, nil, ws.DefaultBranch, sha)
	if in.Push {
		cred, err := s.cred(ctx, wsID)
		if err != nil {
			return sha, err
		}
		if err := s.gitr.Push(ctx, ws.MainPath, ws.DefaultBranch, cred); err != nil {
			return sha, errs.Wrap(err, errs.CodeGitCmdFailed, "git push")
		}
		s.auditPush(ctx, &c.UserID, wsID.String(), ws.DefaultBranch)
	}
	return sha, nil
}

// fireCommitSummary invokes the auto-doc hook best-effort; failures are audited
// but never surfaced to the commit caller.
func (s *gitOpsService) fireCommitSummary(ctx context.Context, userID *uuid.UUID, wsID uuid.UUID, worktreeID *uuid.UUID, branch, sha string) {
	if s.summaryHook == nil || sha == "" {
		return
	}
	if herr := s.summaryHook(ctx, wsID, worktreeID, branch, sha); herr != nil {
		_ = s.rec.Record(ctx, audit.Entry{
			UserID: userID, Action: "docs.summary_enqueue_failed", TargetType: "workspace",
			TargetID: wsID.String(), Metadata: map[string]any{"err": herr.Error()},
		})
	}
}

func (s *gitOpsService) PushOnMain(ctx context.Context, c Caller, wsID uuid.UUID) error {
	ws, err := s.guardWorkspaceWrite(ctx, c, wsID)
	if err != nil {
		return err
	}
	cred, err := s.cred(ctx, wsID)
	if err != nil {
		return err
	}
	if err := s.gitr.Push(ctx, ws.MainPath, ws.DefaultBranch, cred); err != nil {
		return errs.Wrap(err, errs.CodeGitCmdFailed, "git push")
	}
	s.auditPush(ctx, &c.UserID, wsID.String(), ws.DefaultBranch)
	return nil
}

func (s *gitOpsService) LogOnMain(ctx context.Context, c Caller, wsID uuid.UUID, n int) ([]git.Commit, error) {
	ws, err := s.guardWorkspaceRead(ctx, c, wsID)
	if err != nil {
		return nil, err
	}
	commits, err := s.gitr.Log(ctx, ws.MainPath, n)
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeGitCmdFailed, "git log")
	}
	return commits, nil
}

func (s *gitOpsService) DiffOnMain(ctx context.Context, c Caller, wsID uuid.UUID, opts git.DiffOptions) (git.DiffResult, error) {
	ws, err := s.guardWorkspaceRead(ctx, c, wsID)
	if err != nil {
		return git.DiffResult{}, err
	}
	res, err := s.gitr.Diff(ctx, ws.MainPath, opts)
	if err != nil {
		return git.DiffResult{}, errs.Wrap(err, errs.CodeGitCmdFailed, "git diff")
	}
	return res, nil
}

// ===== Worktree =====

func (s *gitOpsService) StatusOnWorktree(ctx context.Context, c Caller, wtID uuid.UUID) ([]git.FileStatus, error) {
	wt, ws, err := s.guardWorktreeRead(ctx, c, wtID)
	if err != nil {
		return nil, err
	}
	_ = ws
	st, err := s.gitr.Status(ctx, wt.Path)
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeGitCmdFailed, "git status")
	}
	return st, nil
}

func (s *gitOpsService) CommitOnWorktree(ctx context.Context, c Caller, wtID uuid.UUID, in CommitInput) (string, error) {
	wt, ws, err := s.guardWorktreeWrite(ctx, c, wtID)
	if err != nil {
		return "", err
	}
	sha, err := s.gitr.Commit(ctx, wt.Path, in.Message, in.AddAll)
	if err != nil {
		return "", errs.Wrap(err, errs.CodeGitCmdFailed, "git commit")
	}
	s.auditCommit(ctx, &c.UserID, ws.ID.String(), wt.Branch, in.Message, sha)
	wtIDCopy := wt.ID
	s.fireCommitSummary(ctx, &c.UserID, ws.ID, &wtIDCopy, wt.Branch, sha)
	if in.Push {
		cred, err := s.cred(ctx, ws.ID)
		if err != nil {
			return sha, err
		}
		if err := s.gitr.Push(ctx, wt.Path, wt.Branch, cred); err != nil {
			return sha, errs.Wrap(err, errs.CodeGitCmdFailed, "git push")
		}
		s.auditPush(ctx, &c.UserID, ws.ID.String(), wt.Branch)
	}
	return sha, nil
}

func (s *gitOpsService) PushOnWorktree(ctx context.Context, c Caller, wtID uuid.UUID) error {
	wt, ws, err := s.guardWorktreeWrite(ctx, c, wtID)
	if err != nil {
		return err
	}
	cred, err := s.cred(ctx, ws.ID)
	if err != nil {
		return err
	}
	if err := s.gitr.Push(ctx, wt.Path, wt.Branch, cred); err != nil {
		return errs.Wrap(err, errs.CodeGitCmdFailed, "git push")
	}
	s.auditPush(ctx, &c.UserID, ws.ID.String(), wt.Branch)
	return nil
}

func (s *gitOpsService) LogOnWorktree(ctx context.Context, c Caller, wtID uuid.UUID, n int) ([]git.Commit, error) {
	wt, _, err := s.guardWorktreeRead(ctx, c, wtID)
	if err != nil {
		return nil, err
	}
	commits, err := s.gitr.Log(ctx, wt.Path, n)
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeGitCmdFailed, "git log")
	}
	return commits, nil
}

func (s *gitOpsService) DiffOnWorktree(ctx context.Context, c Caller, wtID uuid.UUID, opts git.DiffOptions) (git.DiffResult, error) {
	wt, _, err := s.guardWorktreeRead(ctx, c, wtID)
	if err != nil {
		return git.DiffResult{}, err
	}
	res, err := s.gitr.Diff(ctx, wt.Path, opts)
	if err != nil {
		return git.DiffResult{}, errs.Wrap(err, errs.CodeGitCmdFailed, "git diff")
	}
	return res, nil
}

// ===== guards =====

func (s *gitOpsService) guardWorkspaceRead(ctx context.Context, c Caller, wsID uuid.UUID) (*Workspace, error) {
	ws, err := s.repo.GetWorkspaceByID(ctx, wsID)
	if err != nil {
		return nil, err
	}
	if canRead, _, err := s.pa.ResolveByID(ctx, c.UserID, c.IsAdmin, ws.ProjectID); err != nil {
		return nil, err
	} else if !canRead {
		return nil, errs.New(errs.CodeForbidden, "no read access")
	}
	return ws, nil
}

func (s *gitOpsService) guardWorkspaceWrite(ctx context.Context, c Caller, wsID uuid.UUID) (*Workspace, error) {
	ws, err := s.repo.GetWorkspaceByID(ctx, wsID)
	if err != nil {
		return nil, err
	}
	if _, canWrite, err := s.pa.ResolveByID(ctx, c.UserID, c.IsAdmin, ws.ProjectID); err != nil {
		return nil, err
	} else if !canWrite {
		return nil, errs.New(errs.CodeForbidden, "no write access")
	}
	return ws, nil
}

func (s *gitOpsService) guardWorktreeRead(ctx context.Context, c Caller, wtID uuid.UUID) (*Worktree, *Workspace, error) {
	wt, err := s.repo.GetWorktreeByID(ctx, wtID)
	if err != nil {
		return nil, nil, err
	}
	ws, err := s.guardWorkspaceRead(ctx, c, wt.WorkspaceID)
	if err != nil {
		return nil, nil, err
	}
	return wt, ws, nil
}

func (s *gitOpsService) guardWorktreeWrite(ctx context.Context, c Caller, wtID uuid.UUID) (*Worktree, *Workspace, error) {
	wt, err := s.repo.GetWorktreeByID(ctx, wtID)
	if err != nil {
		return nil, nil, err
	}
	ws, err := s.guardWorkspaceWrite(ctx, c, wt.WorkspaceID)
	if err != nil {
		return nil, nil, err
	}
	return wt, ws, nil
}

func (s *gitOpsService) auditCommit(ctx context.Context, uid *uuid.UUID, wsID, target, msg, sha string) {
	_ = s.rec.Record(ctx, audit.Entry{
		UserID: uid, Action: "workspace.commit", TargetType: "workspace", TargetID: wsID,
		Metadata: map[string]any{"target": target, "sha": sha, "msg": msg},
	})
}

func (s *gitOpsService) auditPush(ctx context.Context, uid *uuid.UUID, wsID, branch string) {
	_ = s.rec.Record(ctx, audit.Entry{
		UserID: uid, Action: "workspace.push", TargetType: "workspace", TargetID: wsID,
		Metadata: map[string]any{"branch": branch},
	})
}
