// repository.go is a thin wrapper over the sqlc layer: pgtype<->domain
// conversion, pgx.ErrNoRows -> errs.NotFound translation, and unique-violation
// detection. Mirrors internal/workspace/repository.go.
package changerequest

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"

	changerequestsqlc "github.com/yan1h/agent-coding-workflow/internal/changerequest/sqlc"
	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
)

// Tx is the transaction handle exposed to the service: NextNumber + Create run
// in one tx so per-project numbering is serialized.
type Tx interface {
	NextNumber(ctx context.Context, projectID uuid.UUID) (int, error)
	Create(ctx context.Context, cr *ChangeRequest) (*ChangeRequest, error)
}

// Repository is the change-request module's PG dependency.
type Repository interface {
	GetByID(ctx context.Context, id uuid.UUID) (*ChangeRequest, error)
	GetByNumber(ctx context.Context, projectSlug string, number int) (*ChangeRequest, error)
	ListByWorkspace(ctx context.Context, wsID uuid.UUID) ([]*ChangeRequest, error)
	ListByProject(ctx context.Context, projectID uuid.UUID) ([]*ChangeRequest, error)
	UpdateReview(ctx context.Context, id uuid.UUID, verdict Verdict, reviewedBy uuid.UUID) (*ChangeRequest, error)
	UpdateState(ctx context.Context, id uuid.UUID, st State, mergeSHA string, mergedAt *time.Time) (*ChangeRequest, error)
	UpdateCI(ctx context.Context, id uuid.UUID, ci CIState, st State) (*ChangeRequest, error)
	InTx(ctx context.Context, fn func(Tx) error) error
}

// IsUniqueViolation reports whether err is a PG unique constraint violation.
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    *changerequestsqlc.Queries
}

// NewPostgresRepository builds a Repository from a pgxpool.
func NewPostgresRepository(pool *pgxpool.Pool) Repository {
	return &pgRepo{pool: pool, q: changerequestsqlc.New(pool)}
}

// ===== conversion helpers =====

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{Valid: false}
	}
	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 toPgTimePtr(t *time.Time) pgtype.Timestamptz {
	if t == nil {
		return pgtype.Timestamptz{Valid: false}
	}
	return pgtype.Timestamptz{Time: *t, Valid: true}
}

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

func rowToCR(r changerequestsqlc.ChangeRequest) *ChangeRequest {
	return &ChangeRequest{
		ID:             fromPgUUID(r.ID),
		ProjectID:      fromPgUUID(r.ProjectID),
		WorkspaceID:    fromPgUUID(r.WorkspaceID),
		RequirementID:  fromPgUUIDPtr(r.RequirementID),
		IssueID:        fromPgUUIDPtr(r.IssueID),
		Number:         int(r.Number),
		Title:          r.Title,
		Description:    r.Description,
		SourceBranch:   r.SourceBranch,
		TargetBranch:   r.TargetBranch,
		State:          State(r.State),
		ReviewVerdict:  Verdict(r.ReviewVerdict),
		CIState:        CIState(r.CiState),
		Provider:       r.Provider,
		ExternalID:     r.ExternalID,
		ExternalURL:    r.ExternalUrl,
		MergeCommitSHA: r.MergeCommitSha,
		ReviewedBy:     fromPgUUIDPtr(r.ReviewedBy),
		ReviewedAt:     fromPgTimePtr(r.ReviewedAt),
		CreatedBy:      fromPgUUID(r.CreatedBy),
		MergedAt:       fromPgTimePtr(r.MergedAt),
		CreatedAt:      r.CreatedAt.Time,
		UpdatedAt:      r.UpdatedAt.Time,
	}
}

// ===== queries =====

func (r *pgRepo) GetByID(ctx context.Context, id uuid.UUID) (*ChangeRequest, error) {
	row, err := r.q.GetChangeRequestByID(ctx, toPgUUID(id))
	if errors.Is(err, pgx.ErrNoRows) {
		return nil, errs.New(errs.CodeNotFound, "change request not found")
	}
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "get change request")
	}
	return rowToCR(row), nil
}

func (r *pgRepo) GetByNumber(ctx context.Context, projectSlug string, number int) (*ChangeRequest, error) {
	row, err := r.q.GetChangeRequestByNumber(ctx, changerequestsqlc.GetChangeRequestByNumberParams{
		Slug:   projectSlug,
		Number: int32(number),
	})
	if errors.Is(err, pgx.ErrNoRows) {
		return nil, errs.New(errs.CodeNotFound, "change request not found")
	}
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "get change request by number")
	}
	return rowToCR(row), nil
}

func (r *pgRepo) ListByWorkspace(ctx context.Context, wsID uuid.UUID) ([]*ChangeRequest, error) {
	rows, err := r.q.ListChangeRequestsByWorkspace(ctx, toPgUUID(wsID))
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "list change requests by workspace")
	}
	out := make([]*ChangeRequest, 0, len(rows))
	for _, row := range rows {
		out = append(out, rowToCR(row))
	}
	return out, nil
}

func (r *pgRepo) ListByProject(ctx context.Context, projectID uuid.UUID) ([]*ChangeRequest, error) {
	rows, err := r.q.ListChangeRequestsByProject(ctx, toPgUUID(projectID))
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "list change requests by project")
	}
	out := make([]*ChangeRequest, 0, len(rows))
	for _, row := range rows {
		out = append(out, rowToCR(row))
	}
	return out, nil
}

func (r *pgRepo) UpdateReview(ctx context.Context, id uuid.UUID, verdict Verdict, reviewedBy uuid.UUID) (*ChangeRequest, error) {
	row, err := r.q.UpdateChangeRequestReview(ctx, changerequestsqlc.UpdateChangeRequestReviewParams{
		ID:            toPgUUID(id),
		ReviewVerdict: string(verdict),
		ReviewedBy:    toPgUUID(reviewedBy),
	})
	if errors.Is(err, pgx.ErrNoRows) {
		return nil, errs.New(errs.CodeNotFound, "change request not found")
	}
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "update change request review")
	}
	return rowToCR(row), nil
}

func (r *pgRepo) UpdateState(ctx context.Context, id uuid.UUID, st State, mergeSHA string, mergedAt *time.Time) (*ChangeRequest, error) {
	row, err := r.q.UpdateChangeRequestState(ctx, changerequestsqlc.UpdateChangeRequestStateParams{
		ID:             toPgUUID(id),
		State:          string(st),
		MergeCommitSha: mergeSHA,
		MergedAt:       toPgTimePtr(mergedAt),
	})
	if errors.Is(err, pgx.ErrNoRows) {
		return nil, errs.New(errs.CodeNotFound, "change request not found")
	}
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "update change request state")
	}
	return rowToCR(row), nil
}

func (r *pgRepo) UpdateCI(ctx context.Context, id uuid.UUID, ci CIState, st State) (*ChangeRequest, error) {
	row, err := r.q.UpdateChangeRequestCI(ctx, changerequestsqlc.UpdateChangeRequestCIParams{
		ID:      toPgUUID(id),
		CiState: string(ci),
		State:   string(st),
	})
	if errors.Is(err, pgx.ErrNoRows) {
		return nil, errs.New(errs.CodeNotFound, "change request not found")
	}
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "update change request ci")
	}
	return rowToCR(row), nil
}

func (r *pgRepo) InTx(ctx context.Context, fn func(Tx) error) error {
	return pgx.BeginFunc(ctx, r.pool, func(tx pgx.Tx) error {
		return fn(&pgTx{q: r.q.WithTx(tx)})
	})
}

type pgTx struct{ q *changerequestsqlc.Queries }

func (t *pgTx) NextNumber(ctx context.Context, projectID uuid.UUID) (int, error) {
	n, err := t.q.NextChangeRequestNumber(ctx, toPgUUID(projectID))
	if err != nil {
		return 0, errs.Wrap(err, errs.CodeInternal, "next change request number")
	}
	return int(n), nil
}

func (t *pgTx) Create(ctx context.Context, cr *ChangeRequest) (*ChangeRequest, error) {
	row, err := t.q.CreateChangeRequest(ctx, changerequestsqlc.CreateChangeRequestParams{
		ID:            toPgUUID(cr.ID),
		ProjectID:     toPgUUID(cr.ProjectID),
		WorkspaceID:   toPgUUID(cr.WorkspaceID),
		RequirementID: toPgUUIDPtr(cr.RequirementID),
		IssueID:       toPgUUIDPtr(cr.IssueID),
		Number:        int32(cr.Number),
		Title:         cr.Title,
		Description:   cr.Description,
		SourceBranch:  cr.SourceBranch,
		TargetBranch:  cr.TargetBranch,
		CiState:       string(cr.CIState),
		Provider:      cr.Provider,
		ExternalID:    cr.ExternalID,
		ExternalUrl:   cr.ExternalURL,
		CreatedBy:     toPgUUID(cr.CreatedBy),
	})
	if err != nil {
		if IsUniqueViolation(err) {
			return nil, errs.Wrap(err, errs.CodeConflict, "change request number already used")
		}
		return nil, errs.Wrap(err, errs.CodeInternal, "create change request")
	}
	return rowToCR(row), nil
}
