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

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

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

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

// NewProjectDomainRepository creates a new PostgreSQL project domain repository.
func NewProjectDomainRepository(db *sql.DB) *ProjectDomainRepository {
	return &ProjectDomainRepository{db: db}
}

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

// Create adds a new domain association for a project.
func (r *ProjectDomainRepository) Create(ctx context.Context, pd *domain.ProjectDomain) error {
	now := time.Now()
	err := r.db.QueryRowContext(ctx, `
		INSERT INTO project_domains (project_id, domain, type, dns_record_id, dns_record_type, verified, created_at, updated_at)
		VALUES ($1, $2, $3, $4, $5, $6, $7, $7)
		RETURNING id
	`, pd.ProjectID, strings.ToLower(pd.Domain), pd.Type, nullString(pd.DNSRecordID),
		nullString(pd.DNSRecordType), pd.Verified, now).Scan(&pd.ID)

	if err != nil {
		if strings.Contains(err.Error(), "unique_domain") || strings.Contains(err.Error(), "duplicate key") {
			return fmt.Errorf("domain %q already exists: %w", pd.Domain, domain.ErrDuplicateDomain)
		}
		return fmt.Errorf("create project domain: %w", err)
	}

	pd.CreatedAt = now
	pd.UpdatedAt = now
	return nil
}

// GetByID retrieves a domain by its ID.
func (r *ProjectDomainRepository) GetByID(ctx context.Context, id int64) (*domain.ProjectDomain, error) {
	pd := &domain.ProjectDomain{}
	var dnsRecordID, dnsRecordType sql.NullString

	err := r.db.QueryRowContext(ctx, `
		SELECT id, project_id, domain, type, dns_record_id, dns_record_type, verified, created_at, updated_at
		FROM project_domains
		WHERE id = $1
	`, id).Scan(&pd.ID, &pd.ProjectID, &pd.Domain, &pd.Type, &dnsRecordID, &dnsRecordType,
		&pd.Verified, &pd.CreatedAt, &pd.UpdatedAt)

	if errors.Is(err, sql.ErrNoRows) {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("get project domain by id: %w", err)
	}

	pd.DNSRecordID = dnsRecordID.String
	pd.DNSRecordType = dnsRecordType.String
	return pd, nil
}

// GetByDomain retrieves a domain by its FQDN.
func (r *ProjectDomainRepository) GetByDomain(ctx context.Context, fqdn string) (*domain.ProjectDomain, error) {
	pd := &domain.ProjectDomain{}
	var dnsRecordID, dnsRecordType sql.NullString

	err := r.db.QueryRowContext(ctx, `
		SELECT id, project_id, domain, type, dns_record_id, dns_record_type, verified, created_at, updated_at
		FROM project_domains
		WHERE LOWER(domain) = LOWER($1)
	`, fqdn).Scan(&pd.ID, &pd.ProjectID, &pd.Domain, &pd.Type, &dnsRecordID, &dnsRecordType,
		&pd.Verified, &pd.CreatedAt, &pd.UpdatedAt)

	if errors.Is(err, sql.ErrNoRows) {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("get project domain by fqdn: %w", err)
	}

	pd.DNSRecordID = dnsRecordID.String
	pd.DNSRecordType = dnsRecordType.String
	return pd, nil
}

// ListByProject returns all domains for a project.
func (r *ProjectDomainRepository) ListByProject(ctx context.Context, projectID string) ([]*domain.ProjectDomain, error) {
	rows, err := r.db.QueryContext(ctx, `
		SELECT id, project_id, domain, type, dns_record_id, dns_record_type, verified, created_at, updated_at
		FROM project_domains
		WHERE project_id = $1
		ORDER BY
			CASE type
				WHEN 'primary_auto' THEN 1
				WHEN 'primary_custom' THEN 2
				ELSE 3
			END,
			created_at
	`, projectID)
	if err != nil {
		return nil, fmt.Errorf("list project domains: %w", err)
	}
	defer func() { _ = rows.Close() }()

	var domains []*domain.ProjectDomain
	for rows.Next() {
		pd := &domain.ProjectDomain{}
		var dnsRecordID, dnsRecordType sql.NullString

		if err := rows.Scan(&pd.ID, &pd.ProjectID, &pd.Domain, &pd.Type, &dnsRecordID, &dnsRecordType,
			&pd.Verified, &pd.CreatedAt, &pd.UpdatedAt); err != nil {
			return nil, fmt.Errorf("scan project domain: %w", err)
		}

		pd.DNSRecordID = dnsRecordID.String
		pd.DNSRecordType = dnsRecordType.String
		domains = append(domains, pd)
	}

	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate project domains: %w", err)
	}

	return domains, nil
}

// GetPrimary returns the primary domain for a project.
// Prefers primary_custom over primary_auto if both exist.
func (r *ProjectDomainRepository) GetPrimary(ctx context.Context, projectID string) (*domain.ProjectDomain, error) {
	pd := &domain.ProjectDomain{}
	var dnsRecordID, dnsRecordType sql.NullString

	err := r.db.QueryRowContext(ctx, `
		SELECT id, project_id, domain, type, dns_record_id, dns_record_type, verified, created_at, updated_at
		FROM project_domains
		WHERE project_id = $1 AND type IN ('primary_auto', 'primary_custom')
		ORDER BY CASE type WHEN 'primary_custom' THEN 1 ELSE 2 END
		LIMIT 1
	`, projectID).Scan(&pd.ID, &pd.ProjectID, &pd.Domain, &pd.Type, &dnsRecordID, &dnsRecordType,
		&pd.Verified, &pd.CreatedAt, &pd.UpdatedAt)

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

	pd.DNSRecordID = dnsRecordID.String
	pd.DNSRecordType = dnsRecordType.String
	return pd, nil
}

// Update modifies an existing domain record.
func (r *ProjectDomainRepository) Update(ctx context.Context, pd *domain.ProjectDomain) error {
	result, err := r.db.ExecContext(ctx, `
		UPDATE project_domains
		SET domain = $1, type = $2, dns_record_id = $3, dns_record_type = $4, verified = $5, updated_at = NOW()
		WHERE id = $6
	`, strings.ToLower(pd.Domain), pd.Type, nullString(pd.DNSRecordID),
		nullString(pd.DNSRecordType), pd.Verified, pd.ID)

	if err != nil {
		if strings.Contains(err.Error(), "unique_domain") || strings.Contains(err.Error(), "duplicate key") {
			return fmt.Errorf("domain %q already exists: %w", pd.Domain, domain.ErrDuplicateDomain)
		}
		return fmt.Errorf("update project domain: %w", err)
	}

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

	return nil
}

// Delete removes a domain by ID.
func (r *ProjectDomainRepository) Delete(ctx context.Context, id int64) error {
	result, err := r.db.ExecContext(ctx, `
		DELETE FROM project_domains WHERE id = $1
	`, id)
	if err != nil {
		return fmt.Errorf("delete project domain: %w", err)
	}

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

	return nil
}

// DeleteByDomain removes a domain by FQDN.
func (r *ProjectDomainRepository) DeleteByDomain(ctx context.Context, fqdn string) error {
	result, err := r.db.ExecContext(ctx, `
		DELETE FROM project_domains WHERE LOWER(domain) = LOWER($1)
	`, fqdn)
	if err != nil {
		return fmt.Errorf("delete project domain by fqdn: %w", err)
	}

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

	return nil
}

// DeleteByProject removes all domains for a project.
func (r *ProjectDomainRepository) DeleteByProject(ctx context.Context, projectID string) error {
	_, err := r.db.ExecContext(ctx, `
		DELETE FROM project_domains WHERE project_id = $1
	`, projectID)
	if err != nil {
		return fmt.Errorf("delete project domains: %w", err)
	}

	return nil
}

// Exists checks if a domain already exists.
func (r *ProjectDomainRepository) Exists(ctx context.Context, fqdn string) (bool, error) {
	var exists bool
	err := r.db.QueryRowContext(ctx, `
		SELECT EXISTS(SELECT 1 FROM project_domains WHERE LOWER(domain) = LOWER($1))
	`, fqdn).Scan(&exists)
	if err != nil {
		return false, fmt.Errorf("check domain exists: %w", err)
	}
	return exists, nil
}

// CountByProject returns the number of domains for a project.
func (r *ProjectDomainRepository) CountByProject(ctx context.Context, projectID string) (int, error) {
	var count int
	err := r.db.QueryRowContext(ctx, `
		SELECT COUNT(*) FROM project_domains WHERE project_id = $1
	`, projectID).Scan(&count)
	if err != nil {
		return 0, fmt.Errorf("count project domains: %w", err)
	}
	return count, nil
}

// SlugRepository implements port.SlugGenerator using PostgreSQL.
type SlugRepository struct {
	db         *sql.DB
	maxRetries int
}

// NewSlugRepository creates a new PostgreSQL slug generator.
func NewSlugRepository(db *sql.DB) *SlugRepository {
	return &SlugRepository{db: db, maxRetries: 10}
}

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

// Generate creates a new unique slug.
func (r *SlugRepository) Generate(ctx context.Context) (string, error) {
	for range r.maxRetries {
		slug, err := domain.GenerateSlug()
		if err != nil {
			return "", fmt.Errorf("generate slug: %w", err)
		}

		unique, err := r.IsUnique(ctx, slug)
		if err != nil {
			return "", err
		}

		if unique {
			return slug, nil
		}
	}

	return "", fmt.Errorf("failed to generate unique slug after %d attempts", r.maxRetries)
}

// IsUnique checks if a slug is unique (not in use).
func (r *SlugRepository) IsUnique(ctx context.Context, slug string) (bool, error) {
	var exists bool
	err := r.db.QueryRowContext(ctx, `
		SELECT EXISTS(SELECT 1 FROM projects WHERE slug = $1)
	`, slug).Scan(&exists)
	if err != nil {
		return false, fmt.Errorf("check slug unique: %w", err)
	}
	return !exists, nil
}
