package postgres

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

	"github.com/jmoiron/sqlx"

	"{{GO_MODULE}}/services/{{COMPONENT_NAME}}/internal/domain"
	"{{GO_MODULE}}/services/{{COMPONENT_NAME}}/internal/port"
)

// Compile-time interface check.
var _ port.MediaRepository = (*MediaObjectRepository)(nil)

// mediaObjectRow maps to the media_objects table.
type mediaObjectRow struct {
	ID              string     `db:"id"`
	UserID          string     `db:"user_id"`
	Path            string     `db:"path"`
	Filename        string     `db:"filename"`
	ContentType     string     `db:"content_type"`
	Size            int64      `db:"size"`
	GenerationJobID string     `db:"generation_job_id"`
	DeletedAt       *time.Time `db:"deleted_at"`
	CreatedAt       time.Time  `db:"created_at"`
	UpdatedAt       time.Time  `db:"updated_at"`
}

func (r *mediaObjectRow) toDomain() *domain.MediaObject {
	return &domain.MediaObject{
		ID:              domain.MediaObjectID(r.ID),
		UserID:          domain.UserID(r.UserID),
		Path:            r.Path,
		Filename:        r.Filename,
		ContentType:     r.ContentType,
		Size:            r.Size,
		GenerationJobID: r.GenerationJobID,
		DeletedAt:       r.DeletedAt,
		CreatedAt:       r.CreatedAt,
		UpdatedAt:       r.UpdatedAt,
	}
}

// MediaObjectRepository implements port.MediaRepository with PostgreSQL/CockroachDB.
type MediaObjectRepository struct {
	db *sqlx.DB
}

// NewMediaObjectRepository creates a new Postgres-backed media repository.
func NewMediaObjectRepository(db *sqlx.DB) *MediaObjectRepository {
	return &MediaObjectRepository{db: db}
}

func (r *MediaObjectRepository) Create(ctx context.Context, obj *domain.MediaObject) error {
	_, err := r.db.ExecContext(ctx, `
		INSERT INTO media_objects (id, user_id, path, filename, content_type, size, generation_job_id, created_at, updated_at)
		VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
	`, string(obj.ID), string(obj.UserID), obj.Path, obj.Filename, obj.ContentType,
		obj.Size, obj.GenerationJobID, obj.CreatedAt, obj.UpdatedAt)
	if err != nil {
		return fmt.Errorf("insert media object: %w", err)
	}
	return nil
}

func (r *MediaObjectRepository) Get(ctx context.Context, id domain.MediaObjectID) (*domain.MediaObject, error) {
	var row mediaObjectRow
	err := r.db.GetContext(ctx, &row, `
		SELECT id, user_id, path, filename, content_type, size, generation_job_id, deleted_at, created_at, updated_at
		FROM media_objects WHERE id = $1 AND deleted_at IS NULL
	`, string(id))
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, domain.ErrNotFound
		}
		return nil, fmt.Errorf("get media object: %w", err)
	}
	return row.toDomain(), nil
}

func (r *MediaObjectRepository) ListByUser(ctx context.Context, userID domain.UserID, opts port.ListMediaOptions) ([]domain.MediaObject, int, error) {
	limit := opts.Limit
	if limit <= 0 {
		limit = 50
	}

	// Count total matching records
	countQuery := `SELECT COUNT(*) FROM media_objects WHERE user_id = $1 AND deleted_at IS NULL`
	args := []any{string(userID)}
	argIdx := 2

	if opts.ContentTypePrefix != "" {
		countQuery += fmt.Sprintf(` AND content_type LIKE $%d`, argIdx)
		args = append(args, opts.ContentTypePrefix+"%")
		argIdx++
	}

	var total int
	if err := r.db.GetContext(ctx, &total, countQuery, args...); err != nil {
		return nil, 0, fmt.Errorf("count media objects: %w", err)
	}

	// Fetch paginated results
	query := `
		SELECT id, user_id, path, filename, content_type, size, generation_job_id, deleted_at, created_at, updated_at
		FROM media_objects
		WHERE user_id = $1 AND deleted_at IS NULL`

	fetchArgs := []any{string(userID)}
	fetchIdx := 2

	if opts.ContentTypePrefix != "" {
		query += fmt.Sprintf(` AND content_type LIKE $%d`, fetchIdx)
		fetchArgs = append(fetchArgs, opts.ContentTypePrefix+"%")
		fetchIdx++
	}

	query += ` ORDER BY created_at DESC`
	query += fmt.Sprintf(` LIMIT $%d OFFSET $%d`, fetchIdx, fetchIdx+1)
	fetchArgs = append(fetchArgs, limit, opts.Offset)

	var rows []mediaObjectRow
	if err := r.db.SelectContext(ctx, &rows, query, fetchArgs...); err != nil {
		return nil, 0, fmt.Errorf("list media objects: %w", err)
	}

	objects := make([]domain.MediaObject, len(rows))
	for i := range rows {
		objects[i] = *rows[i].toDomain()
	}
	return objects, total, nil
}

func (r *MediaObjectRepository) SoftDelete(ctx context.Context, id domain.MediaObjectID) error {
	result, err := r.db.ExecContext(ctx, `
		UPDATE media_objects SET deleted_at = NOW(), updated_at = NOW()
		WHERE id = $1 AND deleted_at IS NULL
	`, string(id))
	if err != nil {
		return fmt.Errorf("soft delete media object: %w", err)
	}
	rows, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("soft delete rows affected: %w", err)
	}
	if rows == 0 {
		return domain.ErrNotFound
	}
	return nil
}

func (r *MediaObjectRepository) HardDelete(ctx context.Context, id domain.MediaObjectID) error {
	result, err := r.db.ExecContext(ctx, `DELETE FROM media_objects WHERE id = $1`, string(id))
	if err != nil {
		return fmt.Errorf("hard delete media object: %w", err)
	}
	rows, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("hard delete rows affected: %w", err)
	}
	if rows == 0 {
		return domain.ErrNotFound
	}
	return nil
}

func (r *MediaObjectRepository) GetByPath(ctx context.Context, path string) (*domain.MediaObject, error) {
	var row mediaObjectRow
	err := r.db.GetContext(ctx, &row, `
		SELECT id, user_id, path, filename, content_type, size, generation_job_id, deleted_at, created_at, updated_at
		FROM media_objects WHERE path = $1 AND deleted_at IS NULL
	`, path)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, domain.ErrNotFound
		}
		return nil, fmt.Errorf("get media object by path: %w", err)
	}
	return row.toDomain(), nil
}
