package run

import (
	"context"
	"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"
	runsqlc "github.com/yan1h/agent-coding-workflow/internal/run/sqlc"
)

// Repository 是 run 模块对 PG 的全部依赖。Service / Supervisor 单测通过手写
// fakeRepo 满足。
type Repository interface {
	CreateRunProfile(ctx context.Context, p *RunProfile) (*RunProfile, error)
	GetRunProfileByID(ctx context.Context, id uuid.UUID) (*RunProfile, error)
	ListRunProfilesByWorkspace(ctx context.Context, wsID uuid.UUID) ([]*RunProfile, error)
	UpdateRunProfile(ctx context.Context, p *RunProfile) (*RunProfile, error)
	DeleteRunProfile(ctx context.Context, id uuid.UUID) error
	MarkRunProfileStarted(ctx context.Context, id uuid.UUID) (*RunProfile, error)
	UpdateRunProfileExit(ctx context.Context, id uuid.UUID, exitCode *int32, lastErr string) error
}

// IsUniqueViolation reports whether err is a PG unique constraint violation
// （service 层据此把 (workspace_id, slug) 冲突翻成 409）。
func IsUniqueViolation(err error) bool {
	var pg *pgconn.PgError
	return errors.As(err, &pg) && pg.Code == pgerrcode.UniqueViolation
}

type pgRepo struct {
	q *runsqlc.Queries
}

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

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

func toPgUUID(u uuid.UUID) 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 fromPgTimePtr(t pgtype.Timestamptz) *time.Time {
	if !t.Valid {
		return nil
	}
	v := t.Time
	return &v
}

// normalizeStrSlice 把 nil 归一为非 nil 空切片，避免 pgx 把 nil []string 写成
// SQL NULL 违反 TEXT[] NOT NULL 约束（同 acp/repository.go 的同名 helper）。
func normalizeStrSlice(s []string) []string {
	if s == nil {
		return []string{}
	}
	return s
}

func rowToRunProfile(row runsqlc.WorkspaceRunProfile) *RunProfile {
	return &RunProfile{
		ID:            fromPgUUID(row.ID),
		WorkspaceID:   fromPgUUID(row.WorkspaceID),
		Slug:          row.Slug,
		Name:          row.Name,
		Description:   row.Description,
		Command:       row.Command,
		Args:          row.Args,
		EncryptedEnv:  row.EncryptedEnv,
		Enabled:       row.Enabled,
		LastStartedAt: fromPgTimePtr(row.LastStartedAt),
		LastExitCode:  row.LastExitCode,
		LastError:     row.LastError,
		CreatedBy:     fromPgUUID(row.CreatedBy),
		CreatedAt:     row.CreatedAt.Time,
		UpdatedAt:     row.UpdatedAt.Time,
	}
}

func (r *pgRepo) CreateRunProfile(ctx context.Context, p *RunProfile) (*RunProfile, error) {
	row, err := r.q.CreateRunProfile(ctx, runsqlc.CreateRunProfileParams{
		ID:           toPgUUID(p.ID),
		WorkspaceID:  toPgUUID(p.WorkspaceID),
		Slug:         p.Slug,
		Name:         p.Name,
		Description:  p.Description,
		Command:      p.Command,
		Args:         normalizeStrSlice(p.Args),
		EncryptedEnv: p.EncryptedEnv,
		Enabled:      p.Enabled,
		CreatedBy:    toPgUUID(p.CreatedBy),
	})
	if err != nil {
		if IsUniqueViolation(err) {
			return nil, errs.New(errs.CodeRunProfileSlugTaken, "run profile slug already taken in this workspace")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "create run profile")
	}
	return rowToRunProfile(row), nil
}

func (r *pgRepo) GetRunProfileByID(ctx context.Context, id uuid.UUID) (*RunProfile, error) {
	row, err := r.q.GetRunProfileByID(ctx, toPgUUID(id))
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errs.New(errs.CodeRunProfileNotFound, "run profile not found")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "get run profile")
	}
	return rowToRunProfile(row), nil
}

func (r *pgRepo) ListRunProfilesByWorkspace(ctx context.Context, wsID uuid.UUID) ([]*RunProfile, error) {
	rows, err := r.q.ListRunProfilesByWorkspace(ctx, toPgUUID(wsID))
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "list run profiles")
	}
	out := make([]*RunProfile, 0, len(rows))
	for _, row := range rows {
		out = append(out, rowToRunProfile(row))
	}
	return out, nil
}

func (r *pgRepo) UpdateRunProfile(ctx context.Context, p *RunProfile) (*RunProfile, error) {
	row, err := r.q.UpdateRunProfile(ctx, runsqlc.UpdateRunProfileParams{
		ID:           toPgUUID(p.ID),
		Slug:         p.Slug,
		Name:         p.Name,
		Description:  p.Description,
		Command:      p.Command,
		Args:         normalizeStrSlice(p.Args),
		EncryptedEnv: p.EncryptedEnv,
		Enabled:      p.Enabled,
	})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errs.New(errs.CodeRunProfileNotFound, "run profile not found")
		}
		if IsUniqueViolation(err) {
			return nil, errs.New(errs.CodeRunProfileSlugTaken, "run profile slug already taken in this workspace")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "update run profile")
	}
	return rowToRunProfile(row), nil
}

func (r *pgRepo) DeleteRunProfile(ctx context.Context, id uuid.UUID) error {
	if err := r.q.DeleteRunProfile(ctx, toPgUUID(id)); err != nil {
		return errs.Wrap(err, errs.CodeInternal, "delete run profile")
	}
	return nil
}

func (r *pgRepo) MarkRunProfileStarted(ctx context.Context, id uuid.UUID) (*RunProfile, error) {
	row, err := r.q.MarkRunProfileStarted(ctx, toPgUUID(id))
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errs.New(errs.CodeRunProfileNotFound, "run profile not found")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "mark run profile started")
	}
	return rowToRunProfile(row), nil
}

func (r *pgRepo) UpdateRunProfileExit(ctx context.Context, id uuid.UUID, exitCode *int32, lastErr string) error {
	if err := r.q.UpdateRunProfileExit(ctx, runsqlc.UpdateRunProfileExitParams{
		ID:           toPgUUID(id),
		LastExitCode: exitCode,
		LastError:    lastErr,
	}); err != nil {
		return errs.Wrap(err, errs.CodeInternal, "update run profile exit")
	}
	return nil
}
