// Package awardref stores and updates award reference lists (POTA parks, SOTA
// summits, WWFF references, …). These provide award totals, reference names,
// and per-DXCC filtering. Lists are downloaded from each program's public file.
package awardref

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"strings"
)

// allCols is the column list shared by read queries so they stay in sync.
const allCols = `ref_code, name, dxcc, grp, subgrp, dxcc_list, pattern, valid, valid_from, valid_to, score, bonus, gridsquare, alias`

func encodeDXCCList(l []int) string {
	if len(l) == 0 {
		return ""
	}
	b, err := json.Marshal(l)
	if err != nil {
		return ""
	}
	return string(b)
}

func decodeDXCCList(s string) []int {
	if strings.TrimSpace(s) == "" {
		return nil
	}
	var l []int
	if json.Unmarshal([]byte(s), &l) != nil {
		return nil
	}
	return l
}

// scanRef reads one row selected with allCols into a Ref.
func scanRef(rows *sql.Rows) (Ref, error) {
	var r Ref
	var dxccList string
	var valid int
	if err := rows.Scan(&r.Code, &r.Name, &r.DXCC, &r.Group, &r.SubGrp,
		&dxccList, &r.Pattern, &valid, &r.ValidFrom, &r.ValidTo,
		&r.Score, &r.Bonus, &r.GridSquare, &r.Alias); err != nil {
		return r, err
	}
	r.DXCCList = decodeDXCCList(dxccList)
	r.Valid = valid != 0
	return r, nil
}

// Ref is one award reference. The first five fields are the original schema;
// the rest mirror Log4OM's per-reference editor (group/subgroup, multi-DXCC,
// per-reference regex, validity window, score/bonus, grid, alias).
type Ref struct {
	Code   string `json:"code"`
	Name   string `json:"name"` // description
	DXCC   int    `json:"dxcc"` // primary entity (kept for compatibility / fast filter)
	Group  string `json:"group"`
	SubGrp string `json:"subgrp"`

	DXCCList   []int  `json:"dxcc_list,omitempty"` // all entities this ref is valid for
	Pattern    string `json:"pattern,omitempty"`   // per-reference Go regexp
	Valid      bool   `json:"valid"`               // reference enabled
	ValidFrom  string `json:"valid_from,omitempty"`
	ValidTo    string `json:"valid_to,omitempty"`
	Score      int    `json:"score,omitempty"`
	Bonus      int    `json:"bonus,omitempty"`
	GridSquare string `json:"gridsquare,omitempty"`
	Alias      string `json:"alias,omitempty"`
}

// Repo accesses the award_references table.
type Repo struct{ db *sql.DB }

// NewRepo builds a reference repo on the given connection.
func NewRepo(db *sql.DB) *Repo { return &Repo{db: db} }

// ReplaceAll atomically replaces every reference for one award.
func (r *Repo) ReplaceAll(ctx context.Context, awardCode string, refs []Ref) (int, error) {
	code := strings.ToUpper(strings.TrimSpace(awardCode))
	if code == "" {
		return 0, fmt.Errorf("empty award code")
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return 0, err
	}
	defer tx.Rollback() //nolint:errcheck

	if _, err := tx.ExecContext(ctx, `DELETE FROM award_references WHERE award_code = ?`, code); err != nil {
		return 0, fmt.Errorf("clear refs: %w", err)
	}
	stmt, err := tx.PrepareContext(ctx,
		`REPLACE INTO award_references
		    (award_code, ref_code, name, dxcc, grp, subgrp, dxcc_list, pattern, valid, valid_from, valid_to, score, bonus, gridsquare, alias)
		 VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`)
	if err != nil {
		return 0, err
	}
	defer stmt.Close()

	n := 0
	for _, ref := range refs {
		rc := strings.ToUpper(strings.TrimSpace(ref.Code))
		if rc == "" {
			continue
		}
		// A bulk-replaced list is the authoritative enabled set: store every
		// row as valid. Per-reference disabling is done through Upsert.
		if _, err := stmt.ExecContext(ctx, code, rc, strings.TrimSpace(ref.Name), ref.DXCC, ref.Group, ref.SubGrp,
			encodeDXCCList(ref.DXCCList), ref.Pattern, 1, ref.ValidFrom, ref.ValidTo,
			ref.Score, ref.Bonus, ref.GridSquare, ref.Alias); err != nil {
			return 0, fmt.Errorf("insert ref %s: %w", rc, err)
		}
		n++
	}
	if err := tx.Commit(); err != nil {
		return 0, err
	}
	return n, nil
}

// Count returns how many references an award has stored.
func (r *Repo) Count(ctx context.Context, awardCode string) (int, error) {
	var n int
	err := r.db.QueryRowContext(ctx,
		`SELECT COUNT(*) FROM award_references WHERE award_code = ?`,
		strings.ToUpper(strings.TrimSpace(awardCode))).Scan(&n)
	return n, err
}

// Counts returns reference counts for every award.
func (r *Repo) Counts(ctx context.Context) (map[string]int, error) {
	rows, err := r.db.QueryContext(ctx, `SELECT award_code, COUNT(*) FROM award_references GROUP BY award_code`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := map[string]int{}
	for rows.Next() {
		var code string
		var n int
		if err := rows.Scan(&code, &n); err != nil {
			return nil, err
		}
		out[code] = n
	}
	return out, rows.Err()
}

// NamesFor returns ref_code → name for the given codes of one award (batched so
// we never load a 250k-row map just to label a few worked references).
func (r *Repo) NamesFor(ctx context.Context, awardCode string, codes []string) (map[string]string, error) {
	out := map[string]string{}
	if len(codes) == 0 {
		return out, nil
	}
	code := strings.ToUpper(strings.TrimSpace(awardCode))
	// Chunk to stay under SQLite's parameter limit.
	const chunk = 400
	for start := 0; start < len(codes); start += chunk {
		end := start + chunk
		if end > len(codes) {
			end = len(codes)
		}
		batch := codes[start:end]
		ph := strings.TrimSuffix(strings.Repeat("?,", len(batch)), ",")
		args := make([]any, 0, len(batch)+1)
		args = append(args, code)
		for _, c := range batch {
			args = append(args, strings.ToUpper(strings.TrimSpace(c)))
		}
		rows, err := r.db.QueryContext(ctx,
			`SELECT ref_code, name FROM award_references WHERE award_code = ? AND ref_code IN (`+ph+`)`, args...)
		if err != nil {
			return nil, err
		}
		for rows.Next() {
			var rc, name string
			if err := rows.Scan(&rc, &name); err != nil {
				rows.Close()
				return nil, err
			}
			out[rc] = name
		}
		rows.Close()
	}
	return out, nil
}

// Search returns up to `limit` references of an award matching a code/name
// query, optionally restricted to a DXCC entity. Drives the per-QSO picker.
func (r *Repo) Search(ctx context.Context, awardCode, query string, dxcc, limit int) ([]Ref, error) {
	code := strings.ToUpper(strings.TrimSpace(awardCode))
	q := strings.TrimSpace(query)
	if limit <= 0 || limit > 200 {
		limit = 50
	}
	sqlStr := `SELECT ` + allCols + ` FROM award_references WHERE award_code = ?`
	args := []any{code}
	if dxcc > 0 {
		// Match the primary dxcc OR the multi-DXCC list (JSON contains the id).
		sqlStr += ` AND (dxcc = ? OR dxcc_list LIKE ?)`
		args = append(args, dxcc, fmt.Sprintf("%%%d%%", dxcc))
	}
	if q != "" {
		sqlStr += ` AND (ref_code LIKE ? OR name LIKE ?)`
		args = append(args, "%"+strings.ToUpper(q)+"%", "%"+q+"%")
	}
	sqlStr += ` ORDER BY ref_code LIMIT ?`
	args = append(args, limit)
	rows, err := r.db.QueryContext(ctx, sqlStr, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []Ref
	for rows.Next() {
		ref, err := scanRef(rows)
		if err != nil {
			return nil, err
		}
		out = append(out, ref)
	}
	return out, rows.Err()
}

// List returns every reference of an award, ordered by code. Used by the
// reference editor and (via the engine) to show unworked references.
func (r *Repo) List(ctx context.Context, awardCode string) ([]Ref, error) {
	code := strings.ToUpper(strings.TrimSpace(awardCode))
	rows, err := r.db.QueryContext(ctx,
		`SELECT `+allCols+` FROM award_references WHERE award_code = ? ORDER BY ref_code`, code)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []Ref
	for rows.Next() {
		ref, err := scanRef(rows)
		if err != nil {
			return nil, err
		}
		out = append(out, ref)
	}
	return out, rows.Err()
}

// Upsert inserts or updates a single reference of an award.
func (r *Repo) Upsert(ctx context.Context, awardCode string, ref Ref) error {
	code := strings.ToUpper(strings.TrimSpace(awardCode))
	rc := strings.ToUpper(strings.TrimSpace(ref.Code))
	if code == "" || rc == "" {
		return fmt.Errorf("empty award or reference code")
	}
	_, err := r.db.ExecContext(ctx,
		`REPLACE INTO award_references
		    (award_code, ref_code, name, dxcc, grp, subgrp, dxcc_list, pattern, valid, valid_from, valid_to, score, bonus, gridsquare, alias)
		 VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
		code, rc, strings.TrimSpace(ref.Name), ref.DXCC, ref.Group, ref.SubGrp,
		encodeDXCCList(ref.DXCCList), ref.Pattern, b2i(ref.Valid), ref.ValidFrom, ref.ValidTo,
		ref.Score, ref.Bonus, ref.GridSquare, ref.Alias)
	return err
}

// Rename changes a reference's CODE, keeping everything else about it.
//
// Wanted because a shipped list can simply be wrong: WAJA went out numbered by
// the Japanese state instead of by the JARL, and the only way to correct it was
// to delete all 47 references and import a new list — losing anything the
// operator had adjusted. The number is the one field an editor could not touch.
//
// A rename, not a delete plus an insert: everything the reference carries — its
// pattern, its DXCC list, its validity window — travels with it, which is the
// whole point of correcting a number rather than replacing an entry.
func (r *Repo) Rename(ctx context.Context, awardCode, oldCode, newCode string) error {
	ac := strings.ToUpper(strings.TrimSpace(awardCode))
	from := strings.ToUpper(strings.TrimSpace(oldCode))
	to := strings.ToUpper(strings.TrimSpace(newCode))
	if ac == "" || from == "" || to == "" {
		return fmt.Errorf("empty award or reference code")
	}
	if from == to {
		return nil
	}
	// A collision would REPLACE the other reference and take its name, pattern
	// and dates with it — one silently swallowing another, discovered much later
	// as a reference that has quietly gone missing.
	var n int
	if err := r.db.QueryRowContext(ctx,
		`SELECT COUNT(*) FROM award_references WHERE award_code = ? AND ref_code = ?`, ac, to).Scan(&n); err != nil {
		return err
	}
	if n > 0 {
		return fmt.Errorf("%s already has a reference %s", ac, to)
	}
	res, err := r.db.ExecContext(ctx,
		`UPDATE award_references SET ref_code = ? WHERE award_code = ? AND ref_code = ?`, to, ac, from)
	if err != nil {
		return err
	}
	if rows, _ := res.RowsAffected(); rows == 0 {
		return fmt.Errorf("%s has no reference %s", ac, from)
	}
	return nil
}

// Delete removes one reference from an award.
func (r *Repo) Delete(ctx context.Context, awardCode, refCode string) error {
	_, err := r.db.ExecContext(ctx,
		`DELETE FROM award_references WHERE award_code = ? AND ref_code = ?`,
		strings.ToUpper(strings.TrimSpace(awardCode)), strings.ToUpper(strings.TrimSpace(refCode)))
	return err
}

func b2i(b bool) int {
	if b {
		return 1
	}
	return 0
}
