// workspace_service.go 实现 WorkspaceService。Service 不直接调 git CLI；
// 写路径上调 cloneRunner.Run（异步）/ Runner.Fetch（同步）。
// 鉴权：private project 写要求 owner+admin；internal project 写要求 owner+admin；
// 读权限委托给 ProjectAccess（窄接口，避免对 internal/project 的强依赖）。
package workspace

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"time"

	"github.com/google/uuid"

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

// ProjectAccess 是一个窄接口，让 workspace.Service 不依赖 internal/project 全部。
type ProjectAccess interface {
	Resolve(ctx context.Context, callerID uuid.UUID, isAdmin bool, projectSlug string) (uuid.UUID, bool, bool, error)
	ResolveByID(ctx context.Context, callerID uuid.UUID, isAdmin bool, projectID uuid.UUID) (bool, bool, error)
}

// CloneScheduler 抽象 clone 的异步调度。
type CloneScheduler interface {
	Schedule(wsID uuid.UUID)
}

type workspaceService struct {
	repo        Repository
	rec         audit.Recorder
	enc         *crypto.Encryptor
	gitr        git.Runner
	pa          ProjectAccess
	clones      CloneScheduler
	dataDir     string
	fetchTimout time.Duration
	indexHook   IndexBuildHook
}

// SetIndexBuildHook injects the code-index enqueue callback (app wiring). On Sync
// the workspace marks prior runs stale and enqueues a fresh build at new HEAD.
func (s *workspaceService) SetIndexBuildHook(h IndexBuildHook) { s.indexHook = h }

// NewWorkspaceService 构造 WorkspaceService 实现。
func NewWorkspaceService(
	repo Repository,
	rec audit.Recorder,
	enc *crypto.Encryptor,
	gitr git.Runner,
	pa ProjectAccess,
	clones CloneScheduler,
	dataDir string,
) WorkspaceService {
	return &workspaceService{
		repo:        repo,
		rec:         rec,
		enc:         enc,
		gitr:        gitr,
		pa:          pa,
		clones:      clones,
		dataDir:     dataDir,
		fetchTimout: 5 * time.Minute,
	}
}

func (s *workspaceService) Create(ctx context.Context, c Caller, projectSlug string, in CreateWorkspaceInput) (*Workspace, error) {
	pid, _, canWrite, err := s.pa.Resolve(ctx, c.UserID, c.IsAdmin, projectSlug)
	if err != nil {
		return nil, err
	}
	if !canWrite {
		return nil, errs.New(errs.CodeForbidden, "no write access to project")
	}
	if err := ValidateSlug(in.Slug); err != nil {
		return nil, errs.Wrap(err, errs.CodeInvalidInput, "invalid slug")
	}
	if err := ValidateRemoteURL(in.GitRemoteURL); err != nil {
		return nil, errs.Wrap(err, errs.CodeWorkspaceRemoteInvalid, "invalid remote url")
	}
	if !in.Credential.Kind.IsValid() {
		return nil, errs.New(errs.CodeGitCredentialInvalid, "credential kind invalid")
	}
	defaultBr := in.DefaultBranch
	if defaultBr == "" {
		defaultBr = "main"
	}
	if err := ValidateBranch(defaultBr); err != nil {
		return nil, errs.Wrap(err, errs.CodeInvalidInput, "invalid default branch")
	}
	wsID := uuid.New()
	ws := &Workspace{
		ID:            wsID,
		ProjectID:     pid,
		Slug:          in.Slug,
		Name:          firstNonEmpty(in.Name, in.Slug),
		Description:   in.Description,
		GitRemoteURL:  in.GitRemoteURL,
		DefaultBranch: defaultBr,
		MainPath:      MainPath(s.dataDir, wsID),
		SyncStatus:    SyncStatusCloning,
	}
	created, err := s.repo.CreateWorkspace(ctx, ws)
	if err != nil {
		return nil, err
	}

	cred, err := s.encryptForUpsert(wsID, in.Credential)
	if err != nil {
		return nil, err
	}
	if _, err := s.repo.UpsertCredential(ctx, cred); err != nil {
		return nil, err
	}

	s.audit(ctx, &c.UserID, "workspace.create", "workspace", wsID.String(), map[string]any{"slug": in.Slug})
	if s.clones != nil {
		s.clones.Schedule(wsID)
	}
	return created, nil
}

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

func (s *workspaceService) List(ctx context.Context, c Caller, projectSlug string) ([]*Workspace, error) {
	pid, canRead, _, err := s.pa.Resolve(ctx, c.UserID, c.IsAdmin, projectSlug)
	if err != nil {
		return nil, err
	}
	if !canRead {
		return nil, errs.New(errs.CodeForbidden, "no read access")
	}
	return s.repo.ListWorkspacesByProject(ctx, pid)
}

func (s *workspaceService) Update(ctx context.Context, c Caller, wsID uuid.UUID, in UpdateWorkspaceInput) (*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")
	}
	name := ws.Name
	desc := ws.Description
	defaultBr := ws.DefaultBranch
	if in.Name != nil {
		name = *in.Name
	}
	if in.Description != nil {
		desc = *in.Description
	}
	if in.DefaultBranch != nil && *in.DefaultBranch != ws.DefaultBranch {
		// 切换 default_branch：先校验→拉远端→切到新分支，再写 DB。
		// 任一 git 步骤失败则不改库，让调用方感知并自行重试/回滚。
		if err := ValidateBranch(*in.DefaultBranch); err != nil {
			return nil, errs.Wrap(err, errs.CodeInvalidInput, "invalid default branch")
		}
		cred, credErr := s.loadDecryptedCredential(ctx, wsID)
		if credErr != nil {
			return nil, credErr
		}
		if fetchErr := s.gitr.FetchRemoteBranch(ctx, ws.MainPath, *in.DefaultBranch, cred); fetchErr != nil {
			return nil, errs.Wrap(fetchErr, errs.CodeGitCmdFailed, "fetch remote branch")
		}
		if coErr := s.gitr.Checkout(ctx, ws.MainPath, *in.DefaultBranch); coErr != nil {
			return nil, errs.Wrap(coErr, errs.CodeGitCmdFailed, "checkout branch")
		}
		defaultBr = *in.DefaultBranch
	} else if in.DefaultBranch != nil {
		if err := ValidateBranch(*in.DefaultBranch); err != nil {
			return nil, errs.Wrap(err, errs.CodeInvalidInput, "invalid default branch")
		}
		defaultBr = *in.DefaultBranch
	}
	updated, err := s.repo.UpdateWorkspaceCore(ctx, wsID, name, desc, defaultBr)
	if err != nil {
		return nil, err
	}
	s.audit(ctx, &c.UserID, "workspace.update", "workspace", wsID.String(), nil)
	return updated, nil
}

func (s *workspaceService) Delete(ctx context.Context, c Caller, wsID uuid.UUID) error {
	ws, err := s.repo.GetWorkspaceByID(ctx, wsID)
	if err != nil {
		return err
	}
	if _, canWrite, err := s.pa.ResolveByID(ctx, c.UserID, c.IsAdmin, ws.ProjectID); err != nil {
		return err
	} else if !canWrite {
		return errs.New(errs.CodeForbidden, "no write access")
	}
	if err := s.repo.DeleteWorkspace(ctx, wsID); err != nil {
		return err
	}
	// MainPath 是 ${dataDir}/<wsID>/main；删 workspace 时连父目录（含 .worktrees/）一起清掉。
	go func(path string) {
		_ = os.RemoveAll(path)
	}(filepath.Dir(ws.MainPath))
	s.audit(ctx, &c.UserID, "workspace.delete", "workspace", wsID.String(), nil)
	return nil
}

func (s *workspaceService) Sync(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")
	}
	// 先解密凭据，再抢锁置 syncing：避免置 syncing 后 load 失败留下残留。
	cred, err := s.loadDecryptedCredential(ctx, wsID)
	if err != nil {
		return nil, err
	}
	// 原子 CAS 抢锁（WHERE sync_status IN ('idle','error')），消除"读后写"TOCTOU。
	// 抢到才继续，否则说明已有 clone/sync 在进行中。
	acquired, err := s.repo.MarkSyncing(ctx, wsID)
	if err != nil {
		return nil, err
	}
	if !acquired {
		return nil, errs.New(errs.CodeWorkspaceSyncInProgress, "sync already in progress")
	}
	// 状态回写与请求生命周期解耦：客户端断开/请求超时也要能写回状态，避免永久卡 syncing。
	writeCtx := context.WithoutCancel(ctx)
	// main 仓库目录缺失（历史 clone 从未成功 / 数据卷丢失）时 fetch 注定失败，
	// 转为重新 clone 自愈：状态切回 cloning 并调度 CloneRunner，由其落 idle/error。
	if _, statErr := os.Stat(ws.MainPath); errors.Is(statErr, os.ErrNotExist) {
		if s.clones == nil {
			_, _ = s.repo.UpdateWorkspaceSyncStatus(writeCtx, wsID, SyncStatusError, ws.LastSyncedAt, "main repo missing on disk; clone scheduler unavailable")
			return nil, errs.New(errs.CodeGitCmdFailed, "main repo missing on disk")
		}
		updated, err := s.repo.UpdateWorkspaceSyncStatus(writeCtx, wsID, SyncStatusCloning, ws.LastSyncedAt, "")
		if err != nil {
			return nil, err
		}
		s.audit(writeCtx, &c.UserID, "workspace.reclone", "workspace", wsID.String(), map[string]any{"reason": "main path missing"})
		s.clones.Schedule(wsID)
		return updated, nil
	}
	fetchCtx, cancel := context.WithTimeout(ctx, s.fetchTimout)
	defer cancel()
	if fetchErr := s.gitr.Fetch(fetchCtx, ws.MainPath, cred); fetchErr != nil {
		errMsg := summarizeErr(fetchErr)
		_, _ = s.repo.UpdateWorkspaceSyncStatus(writeCtx, wsID, SyncStatusError, ws.LastSyncedAt, errMsg)
		s.audit(writeCtx, &c.UserID, "workspace.sync_failed", "workspace", wsID.String(), map[string]any{"err": errMsg})
		return nil, errs.Wrap(fetchErr, errs.CodeGitCmdFailed, "git fetch failed")
	}
	// fetch 只更新远端引用，需 ff-only merge 把 main 工作区当前分支快进到远端。
	if mergeErr := s.gitr.MergeFFOnly(fetchCtx, ws.MainPath); mergeErr != nil {
		errMsg := summarizeErr(mergeErr)
		_, _ = s.repo.UpdateWorkspaceSyncStatus(writeCtx, wsID, SyncStatusError, ws.LastSyncedAt, errMsg)
		s.audit(writeCtx, &c.UserID, "workspace.sync_failed", "workspace", wsID.String(), map[string]any{"err": errMsg})
		return nil, errs.Wrap(mergeErr, errs.CodeGitCmdFailed, "git merge --ff-only failed")
	}
	now := time.Now().UTC()
	updated, err := s.repo.UpdateWorkspaceSyncStatus(writeCtx, wsID, SyncStatusIdle, &now, "")
	if err != nil {
		return nil, err
	}
	s.audit(writeCtx, &c.UserID, "workspace.sync", "workspace", wsID.String(), nil)
	// Best-effort: re-index the main worktree at the new HEAD. The hook marks
	// prior runs stale then enqueues a build (coalesced on commit). Never blocks.
	if s.indexHook != nil {
		if herr := s.indexHook(writeCtx, wsID, nil, ""); herr != nil {
			s.audit(writeCtx, &c.UserID, "workspace.index_enqueue_failed", "workspace", wsID.String(), map[string]any{"err": herr.Error()})
		}
	}
	return updated, nil
}

func (s *workspaceService) UpsertCredential(ctx context.Context, c Caller, wsID uuid.UUID, in UpsertCredentialInput) (*Credential, 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")
	}
	if !in.Kind.IsValid() {
		return nil, errs.New(errs.CodeGitCredentialInvalid, "kind invalid")
	}
	cred, err := s.encryptForUpsert(wsID, in)
	if err != nil {
		return nil, err
	}
	saved, err := s.repo.UpsertCredential(ctx, cred)
	if err != nil {
		return nil, err
	}
	s.audit(ctx, &c.UserID, "credential.update", "workspace", wsID.String(), map[string]any{"kind": string(in.Kind)})
	saved.Secret = nil
	return saved, nil
}

func (s *workspaceService) GetCredentialMeta(ctx context.Context, c Caller, wsID uuid.UUID) (*Credential, 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")
	}
	cred, err := s.repo.GetCredentialByWorkspace(ctx, wsID)
	if err != nil {
		return nil, err
	}
	cred.Secret = nil
	return cred, nil
}

func (s *workspaceService) ListBranches(ctx context.Context, c Caller, wsID uuid.UUID) ([]string, 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")
	}
	if ws.MainPath == "" {
		return nil, errs.New(errs.CodeInvalidInput, "workspace not yet cloned")
	}
	if _, statErr := os.Stat(ws.MainPath); errors.Is(statErr, os.ErrNotExist) {
		return nil, errs.New(errs.CodeInvalidInput, "workspace not yet cloned")
	}
	cred, err := s.loadDecryptedCredential(ctx, wsID)
	if err != nil {
		return nil, err
	}
	return s.gitr.ListRemoteBranches(ctx, ws.MainPath, cred)
}

func (s *workspaceService) encryptForUpsert(wsID uuid.UUID, in UpsertCredentialInput) (*Credential, error) {
	cred := &Credential{
		ID:          uuid.New(),
		WorkspaceID: wsID,
		Kind:        in.Kind,
		Username:    in.Username,
	}
	if in.Kind == CredKindNone {
		return cred, nil
	}
	if len(in.Secret) == 0 {
		return nil, errs.New(errs.CodeGitCredentialInvalid, "secret required")
	}
	enc, err := s.enc.Encrypt(in.Secret)
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "encrypt secret")
	}
	cred.Secret = enc
	cred.Fingerprint = fingerprint(in.Kind, in.Secret)
	return cred, nil
}

// CredentialAccessor 让 app 装配代码不重复实现凭据加载。
// CloneRunner 与 GitOpsService 都通过这个窄接口拿明文 git.Credential。
type CredentialAccessor interface {
	LoadDecryptedCredential(ctx context.Context, wsID uuid.UUID) (*git.Credential, error)
}

// SchedulerSetter 让 app 装配代码在创建 cloneRunner 后回填到 service。
// NewWorkspaceService 阶段允许传 nil scheduler；构造完 cloneRunner 再 SetScheduler。
type SchedulerSetter interface {
	SetScheduler(s CloneScheduler)
}

// LoadDecryptedCredential 暴露 loadDecryptedCredential 给 app 装配代码（CloneRunner、GitOps 共用）。
func (s *workspaceService) LoadDecryptedCredential(ctx context.Context, wsID uuid.UUID) (*git.Credential, error) {
	return s.loadDecryptedCredential(ctx, wsID)
}

// SetScheduler 在装配链路把后构造的 CloneRunner 回填到 service（NewWorkspaceService 阶段允许 nil）。
func (s *workspaceService) SetScheduler(sched CloneScheduler) {
	s.clones = sched
}

func (s *workspaceService) loadDecryptedCredential(ctx context.Context, wsID uuid.UUID) (*git.Credential, error) {
	cred, err := s.repo.GetCredentialByWorkspace(ctx, wsID)
	if err != nil {
		var ae *errs.AppError
		if errors.As(err, &ae) && ae.Code == errs.CodeNotFound {
			return &git.Credential{Kind: git.CredentialNone}, nil
		}
		return nil, err
	}
	if cred.Kind == CredKindNone || len(cred.Secret) == 0 {
		return &git.Credential{Kind: git.CredentialNone}, nil
	}
	plain, err := s.enc.Decrypt(cred.Secret)
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "decrypt secret")
	}
	return &git.Credential{
		Kind:     git.CredentialKind(cred.Kind),
		Username: cred.Username,
		Secret:   plain,
	}, nil
}

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

func firstNonEmpty(a, b string) string {
	if a != "" {
		return a
	}
	return b
}

func summarizeErr(err error) string {
	if err == nil {
		return ""
	}
	s := err.Error()
	if len(s) > 1000 {
		return s[:1000] + "...[truncated]"
	}
	return s
}

func fingerprint(kind CredentialKind, secret []byte) string {
	switch kind {
	case CredKindPAT:
		if len(secret) <= 4 {
			return "****"
		}
		return "****" + string(secret[len(secret)-4:])
	case CredKindSSHKey:
		sum := sha256.Sum256(secret)
		return "SHA256:" + hex.EncodeToString(sum[:])[:16]
	default:
		return ""
	}
}

var _ = errors.Is
var _ = fmt.Sprintf
