package orchestrator

import (
	"context"
	"encoding/json"
	"sync"
	"time"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5"

	"github.com/yan1h/agent-coding-workflow/internal/acp"
	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
	"github.com/yan1h/agent-coding-workflow/internal/jobs"
	"github.com/yan1h/agent-coding-workflow/internal/project"
)

// ===== fakeRepo: 内存版 orchestrator.Repository =====

type fakeRepo struct {
	mu    sync.Mutex
	runs  map[uuid.UUID]*Run
	steps map[uuid.UUID]*Step
}

func newFakeRepo() *fakeRepo {
	return &fakeRepo{runs: map[uuid.UUID]*Run{}, steps: map[uuid.UUID]*Step{}}
}

func cloneRun(r *Run) *Run    { c := *r; return &c }
func cloneStep(s *Step) *Step { c := *s; return &c }

func (r *fakeRepo) InsertRun(_ context.Context, run *Run) (*Run, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	if run.ID == uuid.Nil {
		run.ID = uuid.New()
	}
	run.CreatedAt = time.Now()
	run.UpdatedAt = run.CreatedAt
	r.runs[run.ID] = cloneRun(run)
	return cloneRun(run), nil
}

func (r *fakeRepo) GetRun(_ context.Context, id uuid.UUID) (*Run, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	run, ok := r.runs[id]
	if !ok {
		return nil, errs.New(errs.CodeNotFound, "run not found")
	}
	return cloneRun(run), nil
}

func (r *fakeRepo) GetActiveRunByRequirement(_ context.Context, reqID uuid.UUID) (*Run, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	for _, run := range r.runs {
		if run.RequirementID == reqID && run.Status.IsActive() || (run.RequirementID == reqID && run.Status == RunPaused) {
			return cloneRun(run), nil
		}
	}
	return nil, errs.New(errs.CodeNotFound, "no active run")
}

func (r *fakeRepo) ListRuns(_ context.Context, f RunFilter) ([]*Run, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	out := []*Run{}
	for _, run := range r.runs {
		if f.RequirementID != nil && run.RequirementID != *f.RequirementID {
			continue
		}
		if f.Status != "" && run.Status != f.Status {
			continue
		}
		out = append(out, cloneRun(run))
	}
	return out, nil
}

func (r *fakeRepo) UpdateRunStatus(_ context.Context, id uuid.UUID, status RunStatus, lastErr *string) (*Run, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	run, ok := r.runs[id]
	if !ok {
		return nil, errs.New(errs.CodeNotFound, "run not found")
	}
	run.Status = status
	run.LastError = lastErr
	run.UpdatedAt = time.Now()
	return cloneRun(run), nil
}

func (r *fakeRepo) UpdateRunPhase(_ context.Context, id uuid.UUID, phase project.Phase) (*Run, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	run, ok := r.runs[id]
	if !ok {
		return nil, errs.New(errs.CodeNotFound, "run not found")
	}
	run.CurrentPhase = phase
	run.UpdatedAt = time.Now()
	return cloneRun(run), nil
}

func (r *fakeRepo) InsertStep(_ context.Context, s *Step) (*Step, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	if s.ID == uuid.Nil {
		s.ID = uuid.New()
	}
	s.CreatedAt = time.Now()
	s.UpdatedAt = s.CreatedAt
	r.steps[s.ID] = cloneStep(s)
	return cloneStep(s), nil
}

func (r *fakeRepo) GetStep(_ context.Context, id uuid.UUID) (*Step, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	s, ok := r.steps[id]
	if !ok {
		return nil, errs.New(errs.CodeNotFound, "step not found")
	}
	return cloneStep(s), nil
}

func (r *fakeRepo) GetStepBySession(_ context.Context, sessionID uuid.UUID) (*Step, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	for _, s := range r.steps {
		if s.ACPSessionID != nil && *s.ACPSessionID == sessionID {
			return cloneStep(s), nil
		}
	}
	return nil, errs.New(errs.CodeNotFound, "step not found")
}

func (r *fakeRepo) ListStepsByRun(_ context.Context, runID uuid.UUID) ([]*Step, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	out := []*Step{}
	for _, s := range r.steps {
		if s.RunID == runID {
			out = append(out, cloneStep(s))
		}
	}
	return out, nil
}

func (r *fakeRepo) UpdateStepStatus(_ context.Context, s *Step) (*Step, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	cur, ok := r.steps[s.ID]
	if !ok {
		return nil, errs.New(errs.CodeNotFound, "step not found")
	}
	cur.Status = s.Status
	cur.ACPSessionID = s.ACPSessionID
	cur.StopReason = s.StopReason
	cur.GateResult = s.GateResult
	cur.LastError = s.LastError
	cur.JobID = s.JobID
	cur.UpdatedAt = time.Now()
	return cloneStep(cur), nil
}

func (r *fakeRepo) IncrementStepAttempt(_ context.Context, id uuid.UUID) (*Step, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	s, ok := r.steps[id]
	if !ok {
		return nil, errs.New(errs.CodeNotFound, "step not found")
	}
	s.Attempt++
	s.UpdatedAt = time.Now()
	return cloneStep(s), nil
}

func (r *fakeRepo) CountActiveChildSteps(_ context.Context, parentStepID uuid.UUID) (int64, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	var n int64
	for _, s := range r.steps {
		if s.ParentStepID != nil && *s.ParentStepID == parentStepID {
			switch s.Status {
			case StepPending, StepSpawning, StepRunning, StepAwaitingGate:
				n++
			}
		}
	}
	return n, nil
}

func (r *fakeRepo) ResetStuckStepsOnRestart(_ context.Context) ([]*Step, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	out := []*Step{}
	for _, s := range r.steps {
		run, ok := r.runs[s.RunID]
		if !ok {
			continue
		}
		active := run.Status.IsActive() || run.Status == RunPaused
		switch s.Status {
		case StepSpawning, StepRunning, StepAwaitingGate:
			if active {
				s.Status = StepPending
				out = append(out, cloneStep(s))
			}
		}
	}
	return out, nil
}

// fakeRepo 不支持真正的事务：InTx 直接调用 fn（同一实例），WithTx 返回自身。
func (r *fakeRepo) InTx(ctx context.Context, fn func(ctx context.Context, tx pgx.Tx) error) error {
	return fn(ctx, nil)
}
func (r *fakeRepo) WithTx(_ pgx.Tx) Repository { return r }

// ===== fakePM: 内存版 PMAccess =====

type fakePM struct {
	mu          sync.Mutex
	proj        *project.Project
	req         *project.Requirement
	artifacts   []*project.Artifact
	phaseWrites []project.Phase
	phaseErr    error
}

func (p *fakePM) GetProjectByID(_ context.Context, _ uuid.UUID) (*project.Project, error) {
	return p.proj, nil
}
func (p *fakePM) GetProjectBySlug(_ context.Context, _ string) (*project.Project, error) {
	return p.proj, nil
}
func (p *fakePM) GetRequirementByID(_ context.Context, _ uuid.UUID) (*project.Requirement, error) {
	return p.req, nil
}
func (p *fakePM) GetRequirementByNumber(_ context.Context, _ uuid.UUID, _ int) (*project.Requirement, error) {
	return p.req, nil
}
func (p *fakePM) ListLatestArtifacts(_ context.Context, _ uuid.UUID) ([]*project.Artifact, error) {
	return p.artifacts, nil
}
func (p *fakePM) ChangeRequirementPhase(_ context.Context, _ uuid.UUID, _ bool, _ string, _ int, to project.Phase) error {
	p.mu.Lock()
	defer p.mu.Unlock()
	if p.phaseErr != nil {
		return p.phaseErr
	}
	p.phaseWrites = append(p.phaseWrites, to)
	if p.req != nil {
		p.req.Phase = to
	}
	return nil
}

// ===== fakeTurnRunner: acp.TurnRunner =====

type fakeTurnRunner struct {
	stopReason string
	err        error
	resumeErr  error
	created    *acp.Session
	resumed    *acp.Session
	calls      int
}

func (f *fakeTurnRunner) SendPromptAndWait(_ context.Context, sessionID uuid.UUID, _ string) (string, error) {
	f.calls++
	return f.stopReason, f.err
}
func (f *fakeTurnRunner) ResumeSession(_ context.Context, _ acp.Caller, sessionID uuid.UUID) (*acp.Session, error) {
	if f.resumeErr != nil {
		return nil, f.resumeErr
	}
	return &acp.Session{ID: sessionID, Status: acp.SessionStarting}, nil
}

// ===== fakeACPSessions: orchestrator.ACPSessions =====

type fakeACPSessions struct {
	mu        sync.Mutex
	created   []acp.CreateSessionInput
	nextID    uuid.UUID
	createErr error
}

func (f *fakeACPSessions) Create(_ context.Context, _ acp.Caller, in acp.CreateSessionInput) (*acp.Session, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.createErr != nil {
		return nil, f.createErr
	}
	f.created = append(f.created, in)
	id := f.nextID
	if id == uuid.Nil {
		id = uuid.New()
	}
	return &acp.Session{ID: id, Status: acp.SessionStarting, OrchestratorStepID: in.OrchestratorStepID}, nil
}

// ===== fakeLookup: orchestrator.ACPSessionLookup =====

type fakeLookup struct {
	crashed map[uuid.UUID]*acp.Session // stepID → crashed session
	live    map[uuid.UUID]*acp.Session // stepID → live session
}

func newFakeLookup() *fakeLookup {
	return &fakeLookup{crashed: map[uuid.UUID]*acp.Session{}, live: map[uuid.UUID]*acp.Session{}}
}
func (f *fakeLookup) GetSessionByStepID(_ context.Context, stepID uuid.UUID) (*acp.Session, error) {
	if s, ok := f.live[stepID]; ok {
		return s, nil
	}
	return nil, errs.New(errs.CodeNotFound, "no session")
}
func (f *fakeLookup) GetCrashedSessionForResume(_ context.Context, stepID uuid.UUID) (*acp.Session, error) {
	if s, ok := f.crashed[stepID]; ok {
		return s, nil
	}
	return nil, errs.New(errs.CodeNotFound, "no crashed session")
}

// ===== fakeJobs: orchestrator.JobEnqueuer =====

type fakeJobs struct {
	mu       sync.Mutex
	enqueued []stepPayload
}

func (j *fakeJobs) Enqueue(_ context.Context, _ jobs.JobType, payload json.RawMessage, _ time.Time, _ int32) (*jobs.Job, error) {
	j.mu.Lock()
	defer j.mu.Unlock()
	var p stepPayload
	_ = json.Unmarshal(payload, &p)
	j.enqueued = append(j.enqueued, p)
	return &jobs.Job{ID: uuid.New()}, nil
}
func (j *fakeJobs) count() int {
	j.mu.Lock()
	defer j.mu.Unlock()
	return len(j.enqueued)
}

// ===== fakeGate: orchestrator.PhaseGate =====

type fakeGate struct {
	result GateResult
	err    error
}

func (g fakeGate) Evaluate(_ context.Context, _ *Run, _ *Step, _ string) (GateResult, error) {
	return g.result, g.err
}

// ===== fakeAudit: orchestrator.AuditRecorder =====

type fakeAudit struct {
	mu      sync.Mutex
	actions []string
}

func (a *fakeAudit) Record(_ context.Context, _ uuid.UUID, action, _ string, _ map[string]any) {
	a.mu.Lock()
	defer a.mu.Unlock()
	a.actions = append(a.actions, action)
}
func (a *fakeAudit) has(action string) bool {
	a.mu.Lock()
	defer a.mu.Unlock()
	for _, x := range a.actions {
		if x == action {
			return true
		}
	}
	return false
}

// ===== fakeSessTerm: orchestrator.sessionTerminator =====

type fakeSessTerm struct {
	mu         sync.Mutex
	terminated []uuid.UUID
}

func (s *fakeSessTerm) TerminateSession(_ context.Context, _ uuid.UUID, _ bool, sessionID uuid.UUID) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.terminated = append(s.terminated, sessionID)
	return nil
}

// ===== fakeACL: orchestrator.ProjectACL =====

type fakeACL struct {
	allow bool
	read  bool
}

func (a fakeACL) CanRead(_ context.Context, _ uuid.UUID, _ bool, _ uuid.UUID) (bool, error) {
	return a.allow || a.read, nil
}

func (a fakeACL) CanWrite(_ context.Context, _ uuid.UUID, _ bool, _ uuid.UUID) (bool, error) {
	return a.allow, nil
}
