package db

import (
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"time"
)

// ErrPolicyRuleNotFound is returned when a policy rule lookup finds no matching row.
var ErrPolicyRuleNotFound = errors.New("db: policy rule not found")

// RepoMetadata is a repository with aggregate counts for listing.
type RepoMetadata struct {
	Name          string `json:"name"`
	TagCount      int    `json:"tag_count"`
	ManifestCount int    `json:"manifest_count"`
	TotalSize     int64  `json:"total_size"`
	CreatedAt     string `json:"created_at"`
}

// TagInfo is a tag with its manifest digest for repo detail.
type TagInfo struct {
	Name      string `json:"name"`
	Digest    string `json:"digest"`
	UpdatedAt string `json:"updated_at"`
}

// ManifestInfo is a manifest summary for repo detail.
type ManifestInfo struct {
	Digest    string `json:"digest"`
	MediaType string `json:"media_type"`
	Size      int64  `json:"size"`
	CreatedAt string `json:"created_at"`
}

// RepoDetail contains detailed info about a single repository.
type RepoDetail struct {
	Name      string         `json:"name"`
	Tags      []TagInfo      `json:"tags"`
	Manifests []ManifestInfo `json:"manifests"`
	TotalSize int64          `json:"total_size"`
	CreatedAt string         `json:"created_at"`
}

// PolicyRuleRow represents a row from the policy_rules table with parsed JSON.
type PolicyRuleRow struct {
	ID           int64    `json:"id"`
	Priority     int      `json:"priority"`
	Description  string   `json:"description"`
	Effect       string   `json:"effect"`
	Roles        []string `json:"roles,omitempty"`
	AccountTypes []string `json:"account_types,omitempty"`
	SubjectUUID  string   `json:"subject_uuid,omitempty"`
	Actions      []string `json:"actions"`
	Repositories []string `json:"repositories,omitempty"`
	Enabled      bool     `json:"enabled"`
	CreatedBy    string   `json:"created_by,omitempty"`
	CreatedAt    string   `json:"created_at"`
	UpdatedAt    string   `json:"updated_at"`
}

// ListRepositoriesWithMetadata returns all repositories with tag count,
// manifest count, and total size.
func (d *DB) ListRepositoriesWithMetadata(limit, offset int) ([]RepoMetadata, error) {
	if limit <= 0 {
		limit = 50
	}
	rows, err := d.Query(
		`SELECT r.name, r.created_at,
			(SELECT COUNT(*) FROM tags t WHERE t.repository_id = r.id) AS tag_count,
			(SELECT COUNT(*) FROM manifests m WHERE m.repository_id = r.id) AS manifest_count,
			COALESCE((SELECT SUM(m.size) FROM manifests m WHERE m.repository_id = r.id), 0) AS total_size
		 FROM repositories r
		 ORDER BY r.name ASC
		 LIMIT ? OFFSET ?`,
		limit, offset,
	)
	if err != nil {
		return nil, fmt.Errorf("db: list repositories: %w", err)
	}
	defer func() { _ = rows.Close() }()

	var repos []RepoMetadata
	for rows.Next() {
		var r RepoMetadata
		if err := rows.Scan(&r.Name, &r.CreatedAt, &r.TagCount, &r.ManifestCount, &r.TotalSize); err != nil {
			return nil, fmt.Errorf("db: scan repository: %w", err)
		}
		repos = append(repos, r)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("db: iterate repositories: %w", err)
	}
	return repos, nil
}

// GetRepositoryDetail returns detailed information about a repository.
func (d *DB) GetRepositoryDetail(name string) (*RepoDetail, error) {
	var repoID int64
	var createdAt string
	err := d.QueryRow(`SELECT id, created_at FROM repositories WHERE name = ?`, name).Scan(&repoID, &createdAt)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, ErrRepoNotFound
		}
		return nil, fmt.Errorf("db: get repository: %w", err)
	}

	detail := &RepoDetail{Name: name, CreatedAt: createdAt}

	// Tags with manifest digests.
	tagRows, err := d.Query(
		`SELECT t.name, m.digest, t.updated_at
		 FROM tags t JOIN manifests m ON m.id = t.manifest_id
		 WHERE t.repository_id = ?
		 ORDER BY t.name ASC`, repoID,
	)
	if err != nil {
		return nil, fmt.Errorf("db: list repo tags: %w", err)
	}
	defer func() { _ = tagRows.Close() }()

	for tagRows.Next() {
		var ti TagInfo
		if err := tagRows.Scan(&ti.Name, &ti.Digest, &ti.UpdatedAt); err != nil {
			return nil, fmt.Errorf("db: scan tag: %w", err)
		}
		detail.Tags = append(detail.Tags, ti)
	}
	if err := tagRows.Err(); err != nil {
		return nil, fmt.Errorf("db: iterate tags: %w", err)
	}
	if detail.Tags == nil {
		detail.Tags = []TagInfo{}
	}

	// Manifests.
	mRows, err := d.Query(
		`SELECT digest, media_type, size, created_at
		 FROM manifests WHERE repository_id = ?
		 ORDER BY created_at DESC`, repoID,
	)
	if err != nil {
		return nil, fmt.Errorf("db: list repo manifests: %w", err)
	}
	defer func() { _ = mRows.Close() }()

	for mRows.Next() {
		var mi ManifestInfo
		if err := mRows.Scan(&mi.Digest, &mi.MediaType, &mi.Size, &mi.CreatedAt); err != nil {
			return nil, fmt.Errorf("db: scan manifest: %w", err)
		}
		detail.TotalSize += mi.Size
		detail.Manifests = append(detail.Manifests, mi)
	}
	if err := mRows.Err(); err != nil {
		return nil, fmt.Errorf("db: iterate manifests: %w", err)
	}
	if detail.Manifests == nil {
		detail.Manifests = []ManifestInfo{}
	}

	return detail, nil
}

// DeleteRepository deletes a repository and all its manifests, tags, and
// manifest_blobs. CASCADE handles the dependent rows.
func (d *DB) DeleteRepository(name string) error {
	result, err := d.Exec(`DELETE FROM repositories WHERE name = ?`, name)
	if err != nil {
		return fmt.Errorf("db: delete repository: %w", err)
	}
	n, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("db: delete repository rows affected: %w", err)
	}
	if n == 0 {
		return ErrRepoNotFound
	}
	return nil
}

// CreatePolicyRule inserts a new policy rule and returns its ID.
func (d *DB) CreatePolicyRule(rule PolicyRuleRow) (int64, error) {
	body := ruleBody{
		Effect:       rule.Effect,
		Roles:        rule.Roles,
		AccountTypes: rule.AccountTypes,
		SubjectUUID:  rule.SubjectUUID,
		Actions:      rule.Actions,
		Repositories: rule.Repositories,
	}
	ruleJSON, err := json.Marshal(body)
	if err != nil {
		return 0, fmt.Errorf("db: marshal rule body: %w", err)
	}

	enabled := 0
	if rule.Enabled {
		enabled = 1
	}

	result, err := d.Exec(
		`INSERT INTO policy_rules (priority, description, rule_json, enabled, created_by)
		 VALUES (?, ?, ?, ?, ?)`,
		rule.Priority, rule.Description, string(ruleJSON), enabled, nullIfEmpty(rule.CreatedBy),
	)
	if err != nil {
		return 0, fmt.Errorf("db: create policy rule: %w", err)
	}

	id, err := result.LastInsertId()
	if err != nil {
		return 0, fmt.Errorf("db: policy rule last insert id: %w", err)
	}
	return id, nil
}

// GetPolicyRule returns a single policy rule by ID.
func (d *DB) GetPolicyRule(id int64) (*PolicyRuleRow, error) {
	var row PolicyRuleRow
	var ruleJSON string
	var enabledInt int
	var createdBy *string

	err := d.QueryRow(
		`SELECT id, priority, description, rule_json, enabled, created_by, created_at, updated_at
		 FROM policy_rules WHERE id = ?`, id,
	).Scan(&row.ID, &row.Priority, &row.Description, &ruleJSON, &enabledInt, &createdBy, &row.CreatedAt, &row.UpdatedAt)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, ErrPolicyRuleNotFound
		}
		return nil, fmt.Errorf("db: get policy rule: %w", err)
	}

	row.Enabled = enabledInt == 1
	if createdBy != nil {
		row.CreatedBy = *createdBy
	}

	var body ruleBody
	if err := json.Unmarshal([]byte(ruleJSON), &body); err != nil {
		return nil, fmt.Errorf("db: parse rule_json for rule %d: %w", id, err)
	}
	row.Effect = body.Effect
	row.Roles = body.Roles
	row.AccountTypes = body.AccountTypes
	row.SubjectUUID = body.SubjectUUID
	row.Actions = body.Actions
	row.Repositories = body.Repositories

	return &row, nil
}

// ListPolicyRules returns all policy rules ordered by priority ascending.
func (d *DB) ListPolicyRules(limit, offset int) ([]PolicyRuleRow, error) {
	if limit <= 0 {
		limit = 50
	}
	rows, err := d.Query(
		`SELECT id, priority, description, rule_json, enabled, created_by, created_at, updated_at
		 FROM policy_rules
		 ORDER BY priority ASC
		 LIMIT ? OFFSET ?`,
		limit, offset,
	)
	if err != nil {
		return nil, fmt.Errorf("db: list policy rules: %w", err)
	}
	defer func() { _ = rows.Close() }()

	var rules []PolicyRuleRow
	for rows.Next() {
		var row PolicyRuleRow
		var ruleJSON string
		var enabledInt int
		var createdBy *string

		if err := rows.Scan(&row.ID, &row.Priority, &row.Description, &ruleJSON, &enabledInt, &createdBy, &row.CreatedAt, &row.UpdatedAt); err != nil {
			return nil, fmt.Errorf("db: scan policy rule: %w", err)
		}

		row.Enabled = enabledInt == 1
		if createdBy != nil {
			row.CreatedBy = *createdBy
		}

		var body ruleBody
		if err := json.Unmarshal([]byte(ruleJSON), &body); err != nil {
			return nil, fmt.Errorf("db: parse rule_json: %w", err)
		}
		row.Effect = body.Effect
		row.Roles = body.Roles
		row.AccountTypes = body.AccountTypes
		row.SubjectUUID = body.SubjectUUID
		row.Actions = body.Actions
		row.Repositories = body.Repositories

		rules = append(rules, row)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("db: iterate policy rules: %w", err)
	}
	return rules, nil
}

// UpdatePolicyRule performs a partial update of a policy rule.
// Only non-zero/non-empty fields in the input are updated.
// Always updates updated_at.
func (d *DB) UpdatePolicyRule(id int64, updates PolicyRuleRow) error {
	// First check the rule exists.
	var exists int
	err := d.QueryRow(`SELECT COUNT(*) FROM policy_rules WHERE id = ?`, id).Scan(&exists)
	if err != nil {
		return fmt.Errorf("db: check policy rule: %w", err)
	}
	if exists == 0 {
		return ErrPolicyRuleNotFound
	}

	var setClauses []string
	var args []any

	if updates.Priority != 0 {
		setClauses = append(setClauses, "priority = ?")
		args = append(args, updates.Priority)
	}
	if updates.Description != "" {
		setClauses = append(setClauses, "description = ?")
		args = append(args, updates.Description)
	}

	// If any rule body fields are set, rebuild the full rule_json.
	// Read the current value first, apply the update, then write back.
	if updates.Effect != "" || updates.Actions != nil || updates.Roles != nil ||
		updates.AccountTypes != nil || updates.Repositories != nil || updates.SubjectUUID != "" {
		var currentJSON string
		err := d.QueryRow(`SELECT rule_json FROM policy_rules WHERE id = ?`, id).Scan(&currentJSON)
		if err != nil {
			return fmt.Errorf("db: read current rule_json: %w", err)
		}

		var body ruleBody
		if err := json.Unmarshal([]byte(currentJSON), &body); err != nil {
			return fmt.Errorf("db: parse current rule_json: %w", err)
		}

		if updates.Effect != "" {
			body.Effect = updates.Effect
		}
		if updates.Actions != nil {
			body.Actions = updates.Actions
		}
		if updates.Roles != nil {
			body.Roles = updates.Roles
		}
		if updates.AccountTypes != nil {
			body.AccountTypes = updates.AccountTypes
		}
		if updates.Repositories != nil {
			body.Repositories = updates.Repositories
		}
		if updates.SubjectUUID != "" {
			body.SubjectUUID = updates.SubjectUUID
		}

		newJSON, err := json.Marshal(body)
		if err != nil {
			return fmt.Errorf("db: marshal updated rule_json: %w", err)
		}
		setClauses = append(setClauses, "rule_json = ?")
		args = append(args, string(newJSON))
	}

	// Always update updated_at.
	setClauses = append(setClauses, "updated_at = ?")
	args = append(args, time.Now().UTC().Format("2006-01-02T15:04:05Z"))

	query := fmt.Sprintf("UPDATE policy_rules SET %s WHERE id = ?", strings.Join(setClauses, ", "))
	args = append(args, id)

	_, err = d.Exec(query, args...)
	if err != nil {
		return fmt.Errorf("db: update policy rule: %w", err)
	}
	return nil
}

// SetPolicyRuleEnabled sets the enabled flag for a policy rule.
func (d *DB) SetPolicyRuleEnabled(id int64, enabled bool) error {
	enabledInt := 0
	if enabled {
		enabledInt = 1
	}
	result, err := d.Exec(
		`UPDATE policy_rules SET enabled = ?, updated_at = ? WHERE id = ?`,
		enabledInt, time.Now().UTC().Format("2006-01-02T15:04:05Z"), id,
	)
	if err != nil {
		return fmt.Errorf("db: set policy rule enabled: %w", err)
	}
	n, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("db: set policy rule enabled rows affected: %w", err)
	}
	if n == 0 {
		return ErrPolicyRuleNotFound
	}
	return nil
}

// DeletePolicyRule deletes a policy rule by ID.
func (d *DB) DeletePolicyRule(id int64) error {
	result, err := d.Exec(`DELETE FROM policy_rules WHERE id = ?`, id)
	if err != nil {
		return fmt.Errorf("db: delete policy rule: %w", err)
	}
	n, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("db: delete policy rule rows affected: %w", err)
	}
	if n == 0 {
		return ErrPolicyRuleNotFound
	}
	return nil
}
