package db

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

type CreateTagParams struct {
	Name        string
	Slug        string
	Description string
}

type UpdateTagParams struct {
	Name        string
	Slug        string
	Description string
}

func (r *TagRepository) List(ctx context.Context) ([]TagListItem, error) {
	rows, err := r.q.QueryContext(
		ctx,
		`SELECT
			t.id,
			t.name,
			t.slug,
			t.description,
			t.created_at,
			t.updated_at,
			COUNT(DISTINCT pt.project_id) AS project_count
		FROM tags AS t
		LEFT JOIN project_tags AS pt ON pt.tag_id = t.id
		GROUP BY t.id
		ORDER BY t.name COLLATE NOCASE`,
	)
	if err != nil {
		return nil, fmt.Errorf("query tags: %w", err)
	}
	defer rows.Close()

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

		tags = append(tags, *item)
	}

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

	return tags, nil
}

func (r *TagRepository) ListAvailableForProject(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
		WHERE NOT EXISTS (
			SELECT 1
			FROM project_tags AS pt
			WHERE pt.project_id = ?
				AND pt.tag_id = t.id
		)
		ORDER BY t.name COLLATE NOCASE`,
		projectID,
	)
	if err != nil {
		return nil, fmt.Errorf("query available 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 available project tag: %w", err)
		}

		tags = append(tags, *tag)
	}

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

	return tags, nil
}

func (r *TagRepository) GetByID(ctx context.Context, id int64) (*Tag, error) {
	tag, err := scanTag(r.q.QueryRowContext(
		ctx,
		`SELECT id, name, slug, description, created_at, updated_at
		FROM tags
		WHERE id = ?`,
		id,
	))
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, ErrNotFound
		}

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

	return tag, nil
}

func (r *TagRepository) Create(ctx context.Context, params CreateTagParams) (*Tag, error) {
	result, err := r.q.ExecContext(
		ctx,
		`INSERT INTO tags (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 tag: %w", err))
		}

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

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

	return r.GetByID(ctx, tagID)
}

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

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

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

	if rowsAffected == 0 {
		return nil, ErrNotFound
	}

	return r.GetByID(ctx, tagID)
}

func (r *TagRepository) Delete(ctx context.Context, tagID int64) error {
	var projectCount int
	if err := r.q.QueryRowContext(
		ctx,
		`SELECT COUNT(1) FROM project_tags WHERE tag_id = ?`,
		tagID,
	).Scan(&projectCount); err != nil {
		return fmt.Errorf("count tag assignments: %w", err)
	}

	if projectCount > 0 {
		return errors.Join(ErrConflict, fmt.Errorf("tag is assigned to %d project(s)", projectCount))
	}

	result, err := r.q.ExecContext(ctx, `DELETE FROM tags WHERE id = ?`, tagID)
	if err != nil {
		return fmt.Errorf("delete tag: %w", err)
	}

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

	if rowsAffected == 0 {
		return ErrNotFound
	}

	return nil
}

func (r *TagRepository) ListProjects(ctx context.Context, tagID int64) ([]Project, 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
		FROM projects AS p
		INNER JOIN project_tags AS pt ON pt.project_id = p.id
		WHERE pt.tag_id = ?
		ORDER BY p.name COLLATE NOCASE`,
		tagID,
	)
	if err != nil {
		return nil, fmt.Errorf("query tag projects: %w", err)
	}
	defer rows.Close()

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

		projects = append(projects, *project)
	}

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

	return projects, nil
}

func scanTag(scanner rowScanner) (*Tag, error) {
	var (
		tag          Tag
		createdAtRaw string
		updatedAtRaw string
	)

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

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

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

	tag.CreatedAt = createdAt
	tag.UpdatedAt = updatedAt

	return &tag, nil
}

func scanTagListItem(scanner rowScanner) (*TagListItem, error) {
	var (
		item         TagListItem
		createdAtRaw string
		updatedAtRaw string
	)

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

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

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

	item.Tag.CreatedAt = createdAt
	item.Tag.UpdatedAt = updatedAt

	return &item, nil
}
