package orchestrator

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

	"github.com/google/uuid"
	"github.com/jackc/pgerrcode"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgconn"
	"github.com/jackc/pgx/v5/pgtype"
	"github.com/jackc/pgx/v5/pgxpool"

	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
	orchsqlc "github.com/yan1h/agent-coding-workflow/internal/orchestrator/sqlc"
	"github.com/yan1h/agent-coding-workflow/internal/project"
)

// Repository 是 orchestrator 模块对 PG 的全部依赖。Service 单测通过手写 fakeRepo 满足。
type Repository interface {
	// Run
	InsertRun(ctx context.Context, r *Run) (*Run, error)
	GetRun(ctx context.Context, id uuid.UUID) (*Run, error)
	GetActiveRunByRequirement(ctx context.Context, requirementID uuid.UUID) (*Run, error)
	ListRuns(ctx context.Context, f RunFilter) ([]*Run, error)
	UpdateRunStatus(ctx context.Context, id uuid.UUID, status RunStatus, lastErr *string) (*Run, error)
	UpdateRunPhase(ctx context.Context, id uuid.UUID, phase project.Phase) (*Run, error)

	// Step
	InsertStep(ctx context.Context, s *Step) (*Step, error)
	GetStep(ctx context.Context, id uuid.UUID) (*Step, error)
	GetStepBySession(ctx context.Context, sessionID uuid.UUID) (*Step, error)
	ListStepsByRun(ctx context.Context, runID uuid.UUID) ([]*Step, error)
	UpdateStepStatus(ctx context.Context, s *Step) (*Step, error)
	IncrementStepAttempt(ctx context.Context, id uuid.UUID) (*Step, error)
	CountActiveChildSteps(ctx context.Context, parentStepID uuid.UUID) (int64, error)
	ResetStuckStepsOnRestart(ctx context.Context) ([]*Step, error)

	InTx(ctx context.Context, fn func(ctx context.Context, tx pgx.Tx) error) error
	WithTx(tx pgx.Tx) Repository
}

// IsUniqueViolation 报告 err 是否为 PG 唯一约束冲突（用于一 requirement 一活跃 run 的 409 翻译）。
func IsUniqueViolation(err error) bool {
	var pg *pgconn.PgError
	return errors.As(err, &pg) && pg.Code == pgerrcode.UniqueViolation
}

type pgRepo struct {
	pool *pgxpool.Pool
	q    *orchsqlc.Queries
}

// NewPostgresRepository 用 pgxpool 构造 Repository。
func NewPostgresRepository(pool *pgxpool.Pool) Repository {
	return &pgRepo{pool: pool, q: orchsqlc.New(pool)}
}

// InTx 在单事务内执行 fn。nil error commit，否则 rollback。StartRun 用它原子地
// 插入 run + 第一个 step。
func (r *pgRepo) InTx(ctx context.Context, fn func(ctx context.Context, tx pgx.Tx) error) error {
	if r.pool == nil {
		return errs.New(errs.CodeInternal, "orchestrator.Repository.InTx: nil pool (tx-bound instance)")
	}
	return pgx.BeginFunc(ctx, r.pool, func(tx pgx.Tx) error {
		return fn(ctx, tx)
	})
}

// WithTx 返回绑定到指定事务的 Repository 副本。
func (r *pgRepo) WithTx(tx pgx.Tx) Repository {
	return &pgRepo{pool: nil, q: r.q.WithTx(tx)}
}

// ===== 类型转换 helper =====

func toPgUUID(u uuid.UUID) pgtype.UUID { return pgtype.UUID{Bytes: u, Valid: true} }
func toPgUUIDPtr(u *uuid.UUID) pgtype.UUID {
	if u == nil {
		return pgtype.UUID{}
	}
	return pgtype.UUID{Bytes: *u, Valid: true}
}
func fromPgUUID(p pgtype.UUID) uuid.UUID {
	if !p.Valid {
		return uuid.Nil
	}
	return uuid.UUID(p.Bytes)
}
func fromPgUUIDPtr(p pgtype.UUID) *uuid.UUID {
	if !p.Valid {
		return nil
	}
	u := uuid.UUID(p.Bytes)
	return &u
}

func timePtr(p pgtype.Timestamptz) *time.Time {
	if !p.Valid {
		return nil
	}
	t := p.Time
	return &t
}

func marshalConfig(c RunConfig) ([]byte, error) {
	b, err := json.Marshal(c)
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "marshal run config")
	}
	return b, nil
}

func unmarshalConfig(b []byte) RunConfig {
	var c RunConfig
	if len(b) > 0 {
		_ = json.Unmarshal(b, &c)
	}
	return c
}

func marshalGate(g *GateResult) ([]byte, error) {
	if g == nil {
		return nil, nil
	}
	b, err := json.Marshal(g)
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "marshal gate result")
	}
	return b, nil
}

func unmarshalGate(b []byte) *GateResult {
	if len(b) == 0 {
		return nil
	}
	var g GateResult
	if err := json.Unmarshal(b, &g); err != nil {
		return nil
	}
	return &g
}

// ===== Run =====

func (r *pgRepo) InsertRun(ctx context.Context, run *Run) (*Run, error) {
	cfg, err := marshalConfig(run.Config)
	if err != nil {
		return nil, err
	}
	row, err := r.q.InsertRun(ctx, orchsqlc.InsertRunParams{
		ID:            toPgUUID(run.ID),
		ProjectID:     toPgUUID(run.ProjectID),
		RequirementID: toPgUUID(run.RequirementID),
		WorkspaceID:   toPgUUID(run.WorkspaceID),
		OwnerID:       toPgUUID(run.OwnerID),
		Status:        string(run.Status),
		CurrentPhase:  string(run.CurrentPhase),
		Config:        cfg,
		LastError:     run.LastError,
	})
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "insert run")
	}
	return rowToRun(row), nil
}

func (r *pgRepo) GetRun(ctx context.Context, id uuid.UUID) (*Run, error) {
	row, err := r.q.GetRun(ctx, toPgUUID(id))
	if err != nil {
		return nil, pgxNoRowsToNotFound(err, "orchestrator run not found")
	}
	return rowToRun(row), nil
}

func (r *pgRepo) GetActiveRunByRequirement(ctx context.Context, requirementID uuid.UUID) (*Run, error) {
	row, err := r.q.GetActiveRunByRequirement(ctx, toPgUUID(requirementID))
	if err != nil {
		return nil, pgxNoRowsToNotFound(err, "no active orchestrator run for requirement")
	}
	return rowToRun(row), nil
}

func (r *pgRepo) ListRuns(ctx context.Context, f RunFilter) ([]*Run, error) {
	limit := int32(f.Limit)
	if limit <= 0 {
		limit = 100
	}
	rows, err := r.q.ListRuns(ctx, orchsqlc.ListRunsParams{
		Column1: toPgUUIDPtr(f.ProjectID),
		Column2: toPgUUIDPtr(f.RequirementID),
		Column3: string(f.Status),
		Limit:   limit,
	})
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "list runs")
	}
	out := make([]*Run, 0, len(rows))
	for _, row := range rows {
		out = append(out, rowToRun(row))
	}
	return out, nil
}

func (r *pgRepo) UpdateRunStatus(ctx context.Context, id uuid.UUID, status RunStatus, lastErr *string) (*Run, error) {
	row, err := r.q.UpdateRunStatus(ctx, orchsqlc.UpdateRunStatusParams{
		ID:        toPgUUID(id),
		Status:    string(status),
		LastError: lastErr,
	})
	if err != nil {
		return nil, pgxNoRowsToNotFound(err, "orchestrator run not found")
	}
	return rowToRun(row), nil
}

func (r *pgRepo) UpdateRunPhase(ctx context.Context, id uuid.UUID, phase project.Phase) (*Run, error) {
	row, err := r.q.UpdateRunPhase(ctx, orchsqlc.UpdateRunPhaseParams{
		ID:           toPgUUID(id),
		CurrentPhase: string(phase),
	})
	if err != nil {
		return nil, pgxNoRowsToNotFound(err, "orchestrator run not found")
	}
	return rowToRun(row), nil
}

// ===== Step =====

func (r *pgRepo) InsertStep(ctx context.Context, s *Step) (*Step, error) {
	gate, err := marshalGate(s.GateResult)
	if err != nil {
		return nil, err
	}
	row, err := r.q.InsertStep(ctx, orchsqlc.InsertStepParams{
		ID:           toPgUUID(s.ID),
		RunID:        toPgUUID(s.RunID),
		Phase:        string(s.Phase),
		Attempt:      int32(s.Attempt),
		ParentStepID: toPgUUIDPtr(s.ParentStepID),
		Depth:        int32(s.Depth),
		Status:       string(s.Status),
		AgentKindID:  toPgUUID(s.AgentKindID),
		AcpSessionID: toPgUUIDPtr(s.ACPSessionID),
		Prompt:       s.Prompt,
		StopReason:   s.StopReason,
		GateResult:   gate,
		LastError:    s.LastError,
		JobID:        toPgUUIDPtr(s.JobID),
	})
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "insert step")
	}
	return rowToStep(row), nil
}

func (r *pgRepo) GetStep(ctx context.Context, id uuid.UUID) (*Step, error) {
	row, err := r.q.GetStep(ctx, toPgUUID(id))
	if err != nil {
		return nil, pgxNoRowsToNotFound(err, "orchestrator step not found")
	}
	return rowToStep(row), nil
}

func (r *pgRepo) GetStepBySession(ctx context.Context, sessionID uuid.UUID) (*Step, error) {
	row, err := r.q.GetStepBySession(ctx, toPgUUID(sessionID))
	if err != nil {
		return nil, pgxNoRowsToNotFound(err, "orchestrator step not found for session")
	}
	return rowToStep(row), nil
}

func (r *pgRepo) ListStepsByRun(ctx context.Context, runID uuid.UUID) ([]*Step, error) {
	rows, err := r.q.ListStepsByRun(ctx, toPgUUID(runID))
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "list steps by run")
	}
	out := make([]*Step, 0, len(rows))
	for _, row := range rows {
		out = append(out, rowToStep(row))
	}
	return out, nil
}

func (r *pgRepo) UpdateStepStatus(ctx context.Context, s *Step) (*Step, error) {
	gate, err := marshalGate(s.GateResult)
	if err != nil {
		return nil, err
	}
	row, err := r.q.UpdateStepStatus(ctx, orchsqlc.UpdateStepStatusParams{
		ID:           toPgUUID(s.ID),
		Status:       string(s.Status),
		AcpSessionID: toPgUUIDPtr(s.ACPSessionID),
		StopReason:   s.StopReason,
		GateResult:   gate,
		LastError:    s.LastError,
		JobID:        toPgUUIDPtr(s.JobID),
	})
	if err != nil {
		return nil, pgxNoRowsToNotFound(err, "orchestrator step not found")
	}
	return rowToStep(row), nil
}

func (r *pgRepo) IncrementStepAttempt(ctx context.Context, id uuid.UUID) (*Step, error) {
	row, err := r.q.IncrementStepAttempt(ctx, toPgUUID(id))
	if err != nil {
		return nil, pgxNoRowsToNotFound(err, "orchestrator step not found")
	}
	return rowToStep(row), nil
}

func (r *pgRepo) CountActiveChildSteps(ctx context.Context, parentStepID uuid.UUID) (int64, error) {
	n, err := r.q.CountActiveChildSteps(ctx, toPgUUIDPtr(&parentStepID))
	if err != nil {
		return 0, errs.Wrap(err, errs.CodeInternal, "count active child steps")
	}
	return n, nil
}

func (r *pgRepo) ResetStuckStepsOnRestart(ctx context.Context) ([]*Step, error) {
	rows, err := r.q.ResetStuckStepsOnRestart(ctx)
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "reset stuck steps on restart")
	}
	out := make([]*Step, 0, len(rows))
	for _, row := range rows {
		out = append(out, rowToStep(row))
	}
	return out, nil
}

// ===== row → domain =====

func rowToRun(row orchsqlc.OrchestratorRun) *Run {
	return &Run{
		ID:            fromPgUUID(row.ID),
		ProjectID:     fromPgUUID(row.ProjectID),
		RequirementID: fromPgUUID(row.RequirementID),
		WorkspaceID:   fromPgUUID(row.WorkspaceID),
		OwnerID:       fromPgUUID(row.OwnerID),
		Status:        RunStatus(row.Status),
		CurrentPhase:  project.Phase(row.CurrentPhase),
		Config:        unmarshalConfig(row.Config),
		LastError:     row.LastError,
		CreatedAt:     row.CreatedAt.Time,
		UpdatedAt:     row.UpdatedAt.Time,
		EndedAt:       timePtr(row.EndedAt),
	}
}

func rowToStep(row orchsqlc.OrchestratorStep) *Step {
	return &Step{
		ID:           fromPgUUID(row.ID),
		RunID:        fromPgUUID(row.RunID),
		Phase:        project.Phase(row.Phase),
		Attempt:      int(row.Attempt),
		ParentStepID: fromPgUUIDPtr(row.ParentStepID),
		Depth:        int(row.Depth),
		Status:       StepStatus(row.Status),
		AgentKindID:  fromPgUUID(row.AgentKindID),
		ACPSessionID: fromPgUUIDPtr(row.AcpSessionID),
		Prompt:       row.Prompt,
		StopReason:   row.StopReason,
		GateResult:   unmarshalGate(row.GateResult),
		LastError:    row.LastError,
		JobID:        fromPgUUIDPtr(row.JobID),
		CreatedAt:    row.CreatedAt.Time,
		UpdatedAt:    row.UpdatedAt.Time,
		EndedAt:      timePtr(row.EndedAt),
	}
}

func pgxNoRowsToNotFound(err error, msg string) error {
	if errors.Is(err, pgx.ErrNoRows) {
		return errs.New(errs.CodeNotFound, msg)
	}
	return errs.Wrap(err, errs.CodeInternal, msg)
}
