package db

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

type CreateReleaseParams struct {
	ProjectID        int64
	Version          string
	Build            string
	Filename         string
	StoragePath      string
	ChecksumSHA256   string
	SizeBytes        int64
	ContentType      string
	ReleaseNotes     string
	UploadedByUserID *int64
	IsActive         bool
}

func (r *ReleaseRepository) Create(ctx context.Context, params CreateReleaseParams) (*Release, error) {
	isActive := 0
	if params.IsActive {
		isActive = 1
	}

	result, err := r.q.ExecContext(
		ctx,
		`INSERT INTO releases (
			project_id,
			version,
			build,
			filename,
			storage_path,
			checksum_sha256,
			size_bytes,
			content_type,
			release_notes,
			uploaded_by_user_id,
			is_active
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		params.ProjectID,
		strings.TrimSpace(params.Version),
		strings.TrimSpace(params.Build),
		strings.TrimSpace(params.Filename),
		strings.TrimSpace(params.StoragePath),
		strings.TrimSpace(params.ChecksumSHA256),
		params.SizeBytes,
		strings.TrimSpace(params.ContentType),
		strings.TrimSpace(params.ReleaseNotes),
		params.UploadedByUserID,
		isActive,
	)
	if err != nil {
		if isUniqueConstraintError(err) {
			return nil, errors.Join(ErrConflict, fmt.Errorf("insert release: %w", err))
		}

		return nil, fmt.Errorf("insert release: %w", err)
	}

	releaseID, err := result.LastInsertId()
	if err != nil {
		return nil, fmt.Errorf("load inserted release id: %w", err)
	}

	return r.GetByID(ctx, releaseID)
}

func (r *ReleaseRepository) GetByID(ctx context.Context, id int64) (*Release, error) {
	release, err := scanRelease(r.q.QueryRowContext(
		ctx,
		`SELECT
			id,
			project_id,
			version,
			build,
			filename,
			storage_path,
			checksum_sha256,
			size_bytes,
			content_type,
			release_notes,
			created_at,
			updated_at,
			uploaded_by_user_id,
			is_active
		FROM releases
		WHERE id = ?`,
		id,
	))
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, ErrNotFound
		}

		return nil, fmt.Errorf("scan release by id: %w", err)
	}

	return release, nil
}

func (r *ReleaseRepository) GetLatestByProjectID(ctx context.Context, projectID int64) (*Release, error) {
	release, err := scanRelease(r.q.QueryRowContext(
		ctx,
		`SELECT
			id,
			project_id,
			version,
			build,
			filename,
			storage_path,
			checksum_sha256,
			size_bytes,
			content_type,
			release_notes,
			created_at,
			updated_at,
			uploaded_by_user_id,
			is_active
		FROM releases
		WHERE project_id = ?
			AND is_active = 1
		ORDER BY created_at DESC, id DESC
		LIMIT 1`,
		projectID,
	))
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, ErrNotFound
		}

		return nil, fmt.Errorf("scan latest release by project id: %w", err)
	}

	return release, nil
}

func (r *ReleaseRepository) ListByProjectID(ctx context.Context, projectID int64) ([]ReleaseListItem, error) {
	rows, err := r.q.QueryContext(
		ctx,
		`SELECT
			r.id,
			r.project_id,
			r.version,
			r.build,
			r.filename,
			r.storage_path,
			r.checksum_sha256,
			r.size_bytes,
			r.content_type,
			r.release_notes,
			r.created_at,
			r.updated_at,
			r.uploaded_by_user_id,
			r.is_active,
			COALESCE(u.email, '')
		FROM releases AS r
		LEFT JOIN users AS u ON u.id = r.uploaded_by_user_id
		WHERE r.project_id = ?
		ORDER BY r.created_at DESC, r.id DESC`,
		projectID,
	)
	if err != nil {
		return nil, fmt.Errorf("query project releases: %w", err)
	}
	defer rows.Close()

	releases := make([]ReleaseListItem, 0)
	for rows.Next() {
		item, err := scanReleaseListItem(rows)
		if err != nil {
			return nil, fmt.Errorf("scan project release: %w", err)
		}

		releases = append(releases, *item)
	}

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

	return releases, nil
}

func scanRelease(scanner rowScanner) (*Release, error) {
	var (
		release          Release
		createdAtRaw     string
		updatedAtRaw     string
		uploadedByUserID sql.NullInt64
		isActive         int
	)

	if err := scanner.Scan(
		&release.ID,
		&release.ProjectID,
		&release.Version,
		&release.Build,
		&release.Filename,
		&release.StoragePath,
		&release.ChecksumSHA256,
		&release.SizeBytes,
		&release.ContentType,
		&release.ReleaseNotes,
		&createdAtRaw,
		&updatedAtRaw,
		&uploadedByUserID,
		&isActive,
	); err != nil {
		return nil, err
	}

	createdAt, err := parseTimestamp(createdAtRaw)
	if err != nil {
		return nil, fmt.Errorf("parse release created_at: %w", err)
	}

	updatedAt, err := parseTimestamp(updatedAtRaw)
	if err != nil {
		return nil, fmt.Errorf("parse release updated_at: %w", err)
	}

	release.CreatedAt = createdAt
	release.UpdatedAt = updatedAt
	release.IsActive = isActive == 1
	if uploadedByUserID.Valid {
		release.UploadedByUserID = &uploadedByUserID.Int64
	}

	return &release, nil
}

func scanReleaseListItem(scanner rowScanner) (*ReleaseListItem, error) {
	var (
		item             ReleaseListItem
		createdAtRaw     string
		updatedAtRaw     string
		uploadedByUserID sql.NullInt64
		isActive         int
	)

	if err := scanner.Scan(
		&item.Release.ID,
		&item.Release.ProjectID,
		&item.Release.Version,
		&item.Release.Build,
		&item.Release.Filename,
		&item.Release.StoragePath,
		&item.Release.ChecksumSHA256,
		&item.Release.SizeBytes,
		&item.Release.ContentType,
		&item.Release.ReleaseNotes,
		&createdAtRaw,
		&updatedAtRaw,
		&uploadedByUserID,
		&isActive,
		&item.UploadedByEmail,
	); err != nil {
		return nil, err
	}

	createdAt, err := parseTimestamp(createdAtRaw)
	if err != nil {
		return nil, fmt.Errorf("parse release list created_at: %w", err)
	}

	updatedAt, err := parseTimestamp(updatedAtRaw)
	if err != nil {
		return nil, fmt.Errorf("parse release list updated_at: %w", err)
	}

	item.Release.CreatedAt = createdAt
	item.Release.UpdatedAt = updatedAt
	item.Release.IsActive = isActive == 1
	if uploadedByUserID.Valid {
		item.Release.UploadedByUserID = &uploadedByUserID.Int64
	}

	return &item, nil
}
