// Package postgres provides PostgreSQL-based implementations of port interfaces.
package postgres

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"strings"

	"github.com/orchard9/rdev/internal/domain"
	"github.com/orchard9/rdev/internal/port"
)

// CheckoutRepository implements port.CheckoutRepository using PostgreSQL.
type CheckoutRepository struct {
	db *sql.DB
}

// NewCheckoutRepository creates a new PostgreSQL checkout repository.
func NewCheckoutRepository(db *sql.DB) *CheckoutRepository {
	return &CheckoutRepository{db: db}
}

// Ensure CheckoutRepository implements port.CheckoutRepository at compile time.
var _ port.CheckoutRepository = (*CheckoutRepository)(nil)

// Create stores a new checkout record.
func (r *CheckoutRepository) Create(ctx context.Context, checkout *domain.Checkout) error {
	var id string
	err := r.db.QueryRowContext(ctx, `
		INSERT INTO checkouts (
			project_id, branch, feature_slug, gitea_token_id, gitea_token_name,
			clone_url, checked_out_by, checked_out_at, expires_at, status
		)
		VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
		RETURNING id
	`,
		string(checkout.ProjectID),
		checkout.Branch,
		nullString(checkout.FeatureSlug),
		checkout.GiteaTokenID,
		checkout.GiteaTokenName,
		checkout.CloneURL,
		checkout.CheckedOutBy,
		checkout.CheckedOutAt,
		checkout.ExpiresAt,
		string(checkout.Status),
	).Scan(&id)

	if err != nil {
		// Check for unique constraint violation (active checkout exists)
		if isUniqueViolation(err) {
			return domain.ErrCheckoutAlreadyExists
		}
		return fmt.Errorf("insert checkout: %w", err)
	}

	checkout.ID = domain.CheckoutID(id)
	return nil
}

// Get retrieves a checkout by ID.
func (r *CheckoutRepository) Get(ctx context.Context, id domain.CheckoutID) (*domain.Checkout, error) {
	checkout, err := r.scanCheckout(r.db.QueryRowContext(ctx, `
		SELECT id, project_id, branch, feature_slug, gitea_token_id, gitea_token_name,
			   clone_url, checked_out_by, checked_out_at, expires_at, status,
			   checked_in_at, review_task_id
		FROM checkouts
		WHERE id = $1
	`, string(id)))

	if errors.Is(err, sql.ErrNoRows) {
		return nil, domain.ErrCheckoutNotFound
	}
	if err != nil {
		return nil, fmt.Errorf("query checkout: %w", err)
	}
	return checkout, nil
}

// GetByProjectBranch retrieves an active checkout for a project+branch.
func (r *CheckoutRepository) GetByProjectBranch(ctx context.Context, projectID domain.ProjectID, branch string) (*domain.Checkout, error) {
	checkout, err := r.scanCheckout(r.db.QueryRowContext(ctx, `
		SELECT id, project_id, branch, feature_slug, gitea_token_id, gitea_token_name,
			   clone_url, checked_out_by, checked_out_at, expires_at, status,
			   checked_in_at, review_task_id
		FROM checkouts
		WHERE project_id = $1 AND branch = $2 AND status = 'active'
	`, string(projectID), branch))

	if errors.Is(err, sql.ErrNoRows) {
		return nil, domain.ErrCheckoutNotFound
	}
	if err != nil {
		return nil, fmt.Errorf("query checkout by project+branch: %w", err)
	}
	return checkout, nil
}

// List returns checkouts matching the given options.
func (r *CheckoutRepository) List(ctx context.Context, opts domain.CheckoutListOptions) ([]*domain.Checkout, error) {
	query := `
		SELECT id, project_id, branch, feature_slug, gitea_token_id, gitea_token_name,
			   clone_url, checked_out_by, checked_out_at, expires_at, status,
			   checked_in_at, review_task_id
		FROM checkouts
	`
	var args []any
	argIdx := 1

	if opts.Status != nil {
		query += fmt.Sprintf(" WHERE status = $%d", argIdx)
		args = append(args, string(*opts.Status))
		argIdx++
	}

	query += " ORDER BY checked_out_at DESC"

	if opts.Limit > 0 {
		query += fmt.Sprintf(" LIMIT $%d", argIdx)
		args = append(args, opts.Limit)
		argIdx++
	}
	if opts.Offset > 0 {
		query += fmt.Sprintf(" OFFSET $%d", argIdx)
		args = append(args, opts.Offset)
	}

	rows, err := r.db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, fmt.Errorf("query checkouts: %w", err)
	}
	defer func() { _ = rows.Close() }()

	return r.scanCheckouts(rows)
}

// ListByProject returns all checkouts for a project.
func (r *CheckoutRepository) ListByProject(ctx context.Context, projectID domain.ProjectID) ([]*domain.Checkout, error) {
	rows, err := r.db.QueryContext(ctx, `
		SELECT id, project_id, branch, feature_slug, gitea_token_id, gitea_token_name,
			   clone_url, checked_out_by, checked_out_at, expires_at, status,
			   checked_in_at, review_task_id
		FROM checkouts
		WHERE project_id = $1
		ORDER BY checked_out_at DESC
	`, string(projectID))
	if err != nil {
		return nil, fmt.Errorf("query checkouts by project: %w", err)
	}
	defer func() { _ = rows.Close() }()

	return r.scanCheckouts(rows)
}

// UpdateStatus updates the status of a checkout.
func (r *CheckoutRepository) UpdateStatus(ctx context.Context, id domain.CheckoutID, status domain.CheckoutStatus) error {
	result, err := r.db.ExecContext(ctx, `
		UPDATE checkouts
		SET status = $2
		WHERE id = $1
	`, string(id), string(status))
	if err != nil {
		return fmt.Errorf("update checkout status: %w", err)
	}

	rows, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("rows affected: %w", err)
	}
	if rows == 0 {
		return domain.ErrCheckoutNotFound
	}
	return nil
}

// SetCheckedIn marks a checkout as checked in with timestamp.
func (r *CheckoutRepository) SetCheckedIn(ctx context.Context, id domain.CheckoutID, reviewTaskID string) error {
	result, err := r.db.ExecContext(ctx, `
		UPDATE checkouts
		SET status = 'checked_in', checked_in_at = NOW(), review_task_id = $2
		WHERE id = $1 AND status = 'active'
	`, string(id), nullString(reviewTaskID))
	if err != nil {
		return fmt.Errorf("set checked in: %w", err)
	}

	rows, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("rows affected: %w", err)
	}
	if rows == 0 {
		return domain.ErrCheckoutNotActive
	}
	return nil
}

// SetReviewTask sets the review task ID for a checkout.
func (r *CheckoutRepository) SetReviewTask(ctx context.Context, id domain.CheckoutID, taskID string) error {
	result, err := r.db.ExecContext(ctx, `
		UPDATE checkouts
		SET review_task_id = $2
		WHERE id = $1
	`, string(id), taskID)
	if err != nil {
		return fmt.Errorf("set review task: %w", err)
	}

	rows, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("rows affected: %w", err)
	}
	if rows == 0 {
		return domain.ErrCheckoutNotFound
	}
	return nil
}

// CleanupExpired marks expired checkouts and returns their Gitea token IDs for revocation.
func (r *CheckoutRepository) CleanupExpired(ctx context.Context) ([]int64, error) {
	rows, err := r.db.QueryContext(ctx, `
		UPDATE checkouts
		SET status = 'expired'
		WHERE status = 'active' AND expires_at < NOW()
		RETURNING gitea_token_id
	`)
	if err != nil {
		return nil, fmt.Errorf("cleanup expired checkouts: %w", err)
	}
	defer func() { _ = rows.Close() }()

	var tokenIDs []int64
	for rows.Next() {
		var tokenID int64
		if err := rows.Scan(&tokenID); err != nil {
			return nil, fmt.Errorf("scan token id: %w", err)
		}
		tokenIDs = append(tokenIDs, tokenID)
	}

	return tokenIDs, rows.Err()
}

// checkoutScanner is an interface for scanning checkout rows.
// Both sql.Row and sql.Rows implement this via their Scan method.
type checkoutScanner interface {
	Scan(dest ...any) error
}

// scanCheckoutFields scans checkout fields from a scanner into a Checkout struct.
func (r *CheckoutRepository) scanCheckoutFields(scanner checkoutScanner) (*domain.Checkout, error) {
	var (
		checkout     domain.Checkout
		id           string
		projectID    string
		featureSlug  sql.NullString
		status       string
		checkedInAt  sql.NullTime
		reviewTaskID sql.NullString
	)

	err := scanner.Scan(
		&id,
		&projectID,
		&checkout.Branch,
		&featureSlug,
		&checkout.GiteaTokenID,
		&checkout.GiteaTokenName,
		&checkout.CloneURL,
		&checkout.CheckedOutBy,
		&checkout.CheckedOutAt,
		&checkout.ExpiresAt,
		&status,
		&checkedInAt,
		&reviewTaskID,
	)
	if err != nil {
		return nil, err
	}

	checkout.ID = domain.CheckoutID(id)
	checkout.ProjectID = domain.ProjectID(projectID)
	checkout.Status = domain.CheckoutStatus(status)

	if featureSlug.Valid {
		checkout.FeatureSlug = featureSlug.String
	}
	if checkedInAt.Valid {
		checkout.CheckedInAt = &checkedInAt.Time
	}
	if reviewTaskID.Valid {
		checkout.ReviewTaskID = reviewTaskID.String
	}

	return &checkout, nil
}

// scanCheckout scans a single row into a Checkout struct.
func (r *CheckoutRepository) scanCheckout(row *sql.Row) (*domain.Checkout, error) {
	return r.scanCheckoutFields(row)
}

// scanCheckouts scans multiple rows into Checkout structs.
func (r *CheckoutRepository) scanCheckouts(rows *sql.Rows) ([]*domain.Checkout, error) {
	var checkouts []*domain.Checkout

	for rows.Next() {
		checkout, err := r.scanCheckoutFields(rows)
		if err != nil {
			return nil, fmt.Errorf("scan checkout: %w", err)
		}
		checkouts = append(checkouts, checkout)
	}

	return checkouts, rows.Err()
}

// isUniqueViolation checks if the error is a PostgreSQL unique constraint violation.
func isUniqueViolation(err error) bool {
	if err == nil {
		return false
	}
	errStr := err.Error()
	// PostgreSQL error code for unique_violation is 23505
	return strings.Contains(errStr, "unique constraint") ||
		strings.Contains(errStr, "duplicate key") ||
		strings.Contains(errStr, "23505")
}
