package postgres

import (
	"context"
	"time"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5/pgtype"
	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/tdvorak/seen/backend/internal/services/download"
)

const createDownloadJobSQL = `
INSERT INTO download_jobs (
    user_id,
    source_type,
    source,
    title,
    status,
    queue_position,
    progress_percent,
    bytes_total,
    bytes_downloaded,
    download_speed_mbps,
    eta_seconds,
    error_message,
    retry_count,
    created_at,
    updated_at
) VALUES (
    $1::uuid,
    $2::text,
    $3::text,
    COALESCE(NULLIF($4::text, ''), $3::text),
    'queued',
    NULL,
    0,
    0,
    0,
    0,
    NULL,
    '',
    0,
    NOW(),
    NOW()
)
RETURNING
    id::text,
    user_id::text,
    source_type,
    source,
    title,
    status,
    COALESCE(queue_position, 0),
    progress_percent,
    bytes_total,
    bytes_downloaded,
    download_speed_mbps,
    COALESCE(eta_seconds, 0),
    error_message,
    retry_count,
    created_at,
    updated_at;
`

const listDownloadJobsSQL = `
SELECT
    id::text,
    user_id::text,
    source_type,
    source,
    title,
    status,
    COALESCE(queue_position, 0),
    progress_percent,
    bytes_total,
    bytes_downloaded,
    download_speed_mbps,
    COALESCE(eta_seconds, 0),
    error_message,
    retry_count,
    created_at,
    updated_at,
    COALESCE(started_at, NOW()),
    COALESCE(completed_at, NOW()),
    COALESCE(cancelled_at, NOW())
FROM download_jobs
WHERE user_id = $1::uuid
    AND ($2::text = '' OR status = $2::text)
ORDER BY updated_at DESC, created_at DESC
LIMIT $3::int
OFFSET $4::int;
`

const getDownloadJobByIDSQL = `
SELECT
    id::text,
    user_id::text,
    source_type,
    source,
    title,
    status,
    COALESCE(queue_position, 0),
    progress_percent,
    bytes_total,
    bytes_downloaded,
    download_speed_mbps,
    COALESCE(eta_seconds, 0),
    error_message,
    retry_count,
    created_at,
    updated_at,
    COALESCE(started_at, NOW()),
    COALESCE(completed_at, NOW()),
    COALESCE(cancelled_at, NOW())
FROM download_jobs
WHERE user_id = $1::uuid
    AND id = $2::uuid
LIMIT 1;
`

const cancelDownloadJobSQL = `
UPDATE download_jobs
SET
    status = 'cancelled',
    cancelled_at = NOW(),
    updated_at = NOW()
WHERE user_id = $1::uuid
    AND id = $2::uuid
RETURNING
    id::text,
    user_id::text,
    source_type,
    source,
    title,
    status,
    COALESCE(queue_position, 0),
    progress_percent,
    bytes_total,
    bytes_downloaded,
    download_speed_mbps,
    COALESCE(eta_seconds, 0),
    error_message,
    retry_count,
    created_at,
    updated_at,
    COALESCE(started_at, NOW()),
    COALESCE(completed_at, NOW()),
    COALESCE(cancelled_at, NOW());
`

const appendDownloadEventSQL = `
INSERT INTO download_events (
    job_id,
    status,
    message,
    progress_percent,
    payload,
    created_at
) VALUES (
    $1::uuid,
    $2::text,
    $3::text,
    $4::int,
    $5::jsonb,
    NOW()
);
`

const updateDownloadJobSQL = `
UPDATE download_jobs
SET
    status = $3::text,
    progress_percent = $4::int,
    bytes_total = $5::bigint,
    bytes_downloaded = $6::bigint,
    download_speed_mbps = $7::double precision,
    eta_seconds = CASE WHEN $8::int <= 0 THEN NULL ELSE $8::int END,
    error_message = $9::text,
    retry_count = $10::smallint,
    started_at = CASE
        WHEN $3::text IN ('preparing', 'downloading') AND started_at IS NULL THEN NOW()
        ELSE started_at
    END,
    completed_at = CASE
        WHEN $3::text = 'completed' THEN NOW()
        ELSE completed_at
    END,
    cancelled_at = CASE
        WHEN $3::text = 'cancelled' THEN NOW()
        ELSE cancelled_at
    END,
    updated_at = NOW()
WHERE user_id = $1::uuid
    AND id = $2::uuid
RETURNING
    id::text,
    user_id::text,
    source_type,
    source,
    title,
    status,
    COALESCE(queue_position, 0),
    progress_percent,
    bytes_total,
    bytes_downloaded,
    download_speed_mbps,
    COALESCE(eta_seconds, 0),
    error_message,
    retry_count,
    created_at,
    updated_at,
    COALESCE(started_at, NOW()),
    COALESCE(completed_at, NOW()),
    COALESCE(cancelled_at, NOW());
`

const listDownloadEventsSQL = `
SELECT
    id,
    job_id::text,
    status,
    message,
    progress_percent,
    payload::text,
    created_at
FROM download_events
WHERE job_id = $1::uuid
    AND ($2::timestamptz IS NULL OR created_at > $2::timestamptz)
ORDER BY created_at DESC
LIMIT $3::int;
`

type DownloadRepository struct {
	pool *pgxpool.Pool
}

func NewDownloadRepository(pool *pgxpool.Pool) *DownloadRepository {
	return &DownloadRepository{pool: pool}
}

func (r *DownloadRepository) CreateJob(
	ctx context.Context,
	userID uuid.UUID,
	input download.CreateInput,
) (download.Job, error) {
	var job download.Job
	err := r.pool.QueryRow(
		ctx,
		createDownloadJobSQL,
		userID,
		input.SourceType,
		input.Source,
		input.Title,
	).Scan(
		&job.ID,
		&job.UserID,
		&job.SourceType,
		&job.Source,
		&job.Title,
		&job.Status,
		&job.QueuePosition,
		&job.ProgressPercent,
		&job.BytesTotal,
		&job.BytesDownloaded,
		&job.DownloadSpeedMbps,
		&job.EtaSeconds,
		&job.ErrorMessage,
		&job.RetryCount,
		&job.CreatedAt,
		&job.UpdatedAt,
	)
	if err != nil {
		return download.Job{}, err
	}

	return job, nil
}

func (r *DownloadRepository) ListJobs(
	ctx context.Context,
	userID uuid.UUID,
	params download.ListParams,
) ([]download.Job, error) {
	rows, err := r.pool.Query(
		ctx,
		listDownloadJobsSQL,
		userID,
		params.Status,
		params.Limit,
		params.Offset,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	jobs := make([]download.Job, 0, params.Limit)
	for rows.Next() {
		var job download.Job
		if err := rows.Scan(
			&job.ID,
			&job.UserID,
			&job.SourceType,
			&job.Source,
			&job.Title,
			&job.Status,
			&job.QueuePosition,
			&job.ProgressPercent,
			&job.BytesTotal,
			&job.BytesDownloaded,
			&job.DownloadSpeedMbps,
			&job.EtaSeconds,
			&job.ErrorMessage,
			&job.RetryCount,
			&job.CreatedAt,
			&job.UpdatedAt,
			&job.StartedAt,
			&job.CompletedAt,
			&job.CancelledAt,
		); err != nil {
			return nil, err
		}

		jobs = append(jobs, job)
	}

	return jobs, rows.Err()
}

func (r *DownloadRepository) GetJobByID(
	ctx context.Context,
	userID uuid.UUID,
	jobID string,
) (download.Job, error) {
	var job download.Job
	err := r.pool.QueryRow(ctx, getDownloadJobByIDSQL, userID, jobID).Scan(
		&job.ID,
		&job.UserID,
		&job.SourceType,
		&job.Source,
		&job.Title,
		&job.Status,
		&job.QueuePosition,
		&job.ProgressPercent,
		&job.BytesTotal,
		&job.BytesDownloaded,
		&job.DownloadSpeedMbps,
		&job.EtaSeconds,
		&job.ErrorMessage,
		&job.RetryCount,
		&job.CreatedAt,
		&job.UpdatedAt,
		&job.StartedAt,
		&job.CompletedAt,
		&job.CancelledAt,
	)
	if err != nil {
		return download.Job{}, download.ErrNotFound
	}

	return job, nil
}

func (r *DownloadRepository) CancelJob(
	ctx context.Context,
	userID uuid.UUID,
	jobID string,
) (download.Job, error) {
	var job download.Job
	err := r.pool.QueryRow(ctx, cancelDownloadJobSQL, userID, jobID).Scan(
		&job.ID,
		&job.UserID,
		&job.SourceType,
		&job.Source,
		&job.Title,
		&job.Status,
		&job.QueuePosition,
		&job.ProgressPercent,
		&job.BytesTotal,
		&job.BytesDownloaded,
		&job.DownloadSpeedMbps,
		&job.EtaSeconds,
		&job.ErrorMessage,
		&job.RetryCount,
		&job.CreatedAt,
		&job.UpdatedAt,
		&job.StartedAt,
		&job.CompletedAt,
		&job.CancelledAt,
	)
	if err != nil {
		return download.Job{}, download.ErrNotFound
	}

	return job, nil
}

func (r *DownloadRepository) ListEvents(
	ctx context.Context,
	userID uuid.UUID,
	jobID string,
	params download.EventParams,
) ([]download.Event, error) {
	var after pgtype.Timestamptz
	if params.After != "" {
		t, err := time.Parse(time.RFC3339, params.After)
		if err == nil {
			after = pgtype.Timestamptz{
				Time:  t,
				Valid: true,
			}
		}
	}

	rows, err := r.pool.Query(ctx, listDownloadEventsSQL, jobID, after, params.Limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	events := make([]download.Event, 0, params.Limit)
	for rows.Next() {
		var event download.Event
		if err := rows.Scan(
			&event.ID,
			&event.JobID,
			&event.Status,
			&event.Message,
			&event.ProgressPercent,
			&event.Payload,
			&event.CreatedAt,
		); err != nil {
			return nil, err
		}

		events = append(events, event)
	}

	return events, rows.Err()
}

func (r *DownloadRepository) AppendEvent(
	ctx context.Context,
	jobID string,
	event download.Event,
) error {
	_, err := r.pool.Exec(
		ctx,
		appendDownloadEventSQL,
		jobID,
		event.Status,
		event.Message,
		event.ProgressPercent,
		event.Payload,
	)
	return err
}

func (r *DownloadRepository) UpdateJob(
	ctx context.Context,
	userID uuid.UUID,
	jobID string,
	input download.UpdateInput,
) (download.Job, error) {
	var job download.Job
	err := r.pool.QueryRow(
		ctx,
		updateDownloadJobSQL,
		userID,
		jobID,
		input.Status,
		input.ProgressPercent,
		input.BytesTotal,
		input.BytesDownloaded,
		input.DownloadSpeedMbps,
		input.EtaSeconds,
		input.ErrorMessage,
		input.RetryCount,
	).Scan(
		&job.ID,
		&job.UserID,
		&job.SourceType,
		&job.Source,
		&job.Title,
		&job.Status,
		&job.QueuePosition,
		&job.ProgressPercent,
		&job.BytesTotal,
		&job.BytesDownloaded,
		&job.DownloadSpeedMbps,
		&job.EtaSeconds,
		&job.ErrorMessage,
		&job.RetryCount,
		&job.CreatedAt,
		&job.UpdatedAt,
		&job.StartedAt,
		&job.CompletedAt,
		&job.CancelledAt,
	)
	if err != nil {
		return download.Job{}, download.ErrNotFound
	}

	return job, nil
}
