package db

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

type CreateProjectParams struct {
	Name        string
	Slug        string
	Description string
}

type UpdateProjectParams struct {
	Name        string
	Slug        string
	Description string
}

func (r *ProjectRepository) List(ctx context.Context) ([]ProjectListItem, error) {
	rows, err := r.q.QueryContext(
		ctx,
		`SELECT
			p.id,
			p.name,
			p.slug,
			p.description,
			p.is_active,
			p.created_at,
			p.updated_at,
			COUNT(DISTINCT pt.tag_id) AS tag_count,
			COUNT(DISTINCT rel.id) AS release_count
		FROM projects AS p
		LEFT JOIN project_tags AS pt ON pt.project_id = p.id
		LEFT JOIN releases AS rel ON rel.project_id = p.id AND rel.is_active = 1
		GROUP BY p.id
		ORDER BY p.is_active DESC, p.updated_at DESC, p.created_at DESC, p.name COLLATE NOCASE`,
	)
	if err != nil {
		return nil, fmt.Errorf("query projects: %w", err)
	}
	defer rows.Close()

	projects := make([]ProjectListItem, 0)
	for rows.Next() {
		item, err := scanProjectListItem(rows)
		if err != nil {
			return nil, fmt.Errorf("scan project list item: %w", err)
		}

		projects = append(projects, *item)
	}

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

	return projects, nil
}

func (r *ProjectRepository) GetByID(ctx context.Context, id int64) (*Project, error) {
	project, err := scanProject(r.q.QueryRowContext(
		ctx,
		`SELECT id, name, slug, description, is_active, created_at, updated_at
		FROM projects
		WHERE id = ?`,
		id,
	))
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, ErrNotFound
		}

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

	return project, nil
}

func (r *ProjectRepository) GetBySlug(ctx context.Context, slug string) (*Project, error) {
	project, err := scanProject(r.q.QueryRowContext(
		ctx,
		`SELECT id, name, slug, description, is_active, created_at, updated_at
		FROM projects
		WHERE slug = ?
		LIMIT 1`,
		strings.TrimSpace(slug),
	))
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, ErrNotFound
		}

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

	return project, nil
}

func (r *ProjectRepository) Create(ctx context.Context, params CreateProjectParams) (*Project, error) {
	result, err := r.q.ExecContext(
		ctx,
		`INSERT INTO projects (name, slug, description) VALUES (?, ?, ?)`,
		strings.TrimSpace(params.Name),
		strings.TrimSpace(params.Slug),
		strings.TrimSpace(params.Description),
	)
	if err != nil {
		if isUniqueConstraintError(err) {
			return nil, errors.Join(ErrConflict, fmt.Errorf("insert project: %w", err))
		}

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

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

	return r.GetByID(ctx, projectID)
}

func (r *ProjectRepository) Update(ctx context.Context, projectID int64, params UpdateProjectParams) (*Project, error) {
	result, err := r.q.ExecContext(
		ctx,
		`UPDATE projects
		SET name = ?, slug = ?, description = ?
		WHERE id = ?`,
		strings.TrimSpace(params.Name),
		strings.TrimSpace(params.Slug),
		strings.TrimSpace(params.Description),
		projectID,
	)
	if err != nil {
		if isUniqueConstraintError(err) {
			return nil, errors.Join(ErrConflict, fmt.Errorf("update project: %w", err))
		}

		return nil, fmt.Errorf("update project: %w", err)
	}

	rowsAffected, err := result.RowsAffected()
	if err != nil {
		return nil, fmt.Errorf("read updated project rows: %w", err)
	}

	if rowsAffected == 0 {
		return nil, ErrNotFound
	}

	return r.GetByID(ctx, projectID)
}

func (r *ProjectRepository) SetActive(ctx context.Context, projectID int64, isActive bool) error {
	activeValue := 0
	if isActive {
		activeValue = 1
	}

	result, err := r.q.ExecContext(
		ctx,
		`UPDATE projects
		SET is_active = ?
		WHERE id = ?`,
		activeValue,
		projectID,
	)
	if err != nil {
		return fmt.Errorf("update project active state: %w", err)
	}

	rowsAffected, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("read affected project rows: %w", err)
	}

	if rowsAffected == 0 {
		return ErrNotFound
	}

	return nil
}

func (r *ProjectRepository) ListTags(ctx context.Context, projectID int64) ([]Tag, error) {
	rows, err := r.q.QueryContext(
		ctx,
		`SELECT
			t.id,
			t.name,
			t.slug,
			t.description,
			t.created_at,
			t.updated_at
		FROM tags AS t
		INNER JOIN project_tags AS pt ON pt.tag_id = t.id
		WHERE pt.project_id = ?
		ORDER BY t.name COLLATE NOCASE`,
		projectID,
	)
	if err != nil {
		return nil, fmt.Errorf("query project tags: %w", err)
	}
	defer rows.Close()

	tags := make([]Tag, 0)
	for rows.Next() {
		tag, err := scanTag(rows)
		if err != nil {
			return nil, fmt.Errorf("scan project tag: %w", err)
		}

		tags = append(tags, *tag)
	}

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

	return tags, nil
}

func (r *ProjectRepository) AttachTag(ctx context.Context, projectID, tagID int64) error {
	if _, err := r.q.ExecContext(
		ctx,
		`INSERT OR IGNORE INTO project_tags (project_id, tag_id) VALUES (?, ?)`,
		projectID,
		tagID,
	); err != nil {
		return fmt.Errorf("insert project tag link: %w", err)
	}

	return nil
}

func (r *ProjectRepository) DetachTag(ctx context.Context, projectID, tagID int64) error {
	if _, err := r.q.ExecContext(
		ctx,
		`DELETE FROM project_tags WHERE project_id = ? AND tag_id = ?`,
		projectID,
		tagID,
	); err != nil {
		return fmt.Errorf("delete project tag link: %w", err)
	}

	return nil
}

func scanProject(scanner rowScanner) (*Project, error) {
	var (
		project      Project
		isActive     int
		createdAtRaw string
		updatedAtRaw string
	)

	if err := scanner.Scan(
		&project.ID,
		&project.Name,
		&project.Slug,
		&project.Description,
		&isActive,
		&createdAtRaw,
		&updatedAtRaw,
	); err != nil {
		return nil, err
	}

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

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

	project.IsActive = isActive == 1
	project.CreatedAt = createdAt
	project.UpdatedAt = updatedAt

	return &project, nil
}

func scanProjectListItem(scanner rowScanner) (*ProjectListItem, error) {
	var (
		item         ProjectListItem
		isActive     int
		createdAtRaw string
		updatedAtRaw string
	)

	if err := scanner.Scan(
		&item.Project.ID,
		&item.Project.Name,
		&item.Project.Slug,
		&item.Project.Description,
		&isActive,
		&createdAtRaw,
		&updatedAtRaw,
		&item.TagCount,
		&item.ReleaseCount,
	); err != nil {
		return nil, err
	}

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

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

	item.Project.IsActive = isActive == 1
	item.Project.CreatedAt = createdAt
	item.Project.UpdatedAt = updatedAt

	return &item, nil
}
