// Package cabrillo writes a Cabrillo v3 contest log from OpsLog QSOs — the
// format most contest sponsors require for log submission (a header of
// CATEGORY-* / CONTEST / CALLSIGN tags followed by one "QSO:" line each).
package cabrillo

import (
	"fmt"
	"io"
	"strings"

	"hamlog/internal/qso"
)

// Header holds the Cabrillo header fields. Only Callsign and Contest really
// matter; the rest carry sensible defaults the operator can edit in the file
// before submitting (categories vary per contest).
type Header struct {
	Callsign    string
	Contest     string
	Operators   string
	CatOperator string // SINGLE-OP | MULTI-OP | CHECKLOG
	CatBand     string // ALL | 20M | …
	CatMode     string // CW | SSB | MIXED | …
	CatPower    string // HIGH | LOW | QRP
	GridLocator string
	Name        string
	Email       string
	Club        string
}

func def(v, d string) string {
	if strings.TrimSpace(v) == "" {
		return d
	}
	return v
}

// Generate writes the Cabrillo document for qsos and returns the number of QSO
// lines written (rows without a usable frequency/callsign are skipped).
func Generate(w io.Writer, h Header, qsos []qso.QSO) (int, error) {
	myCall := strings.ToUpper(strings.TrimSpace(h.Callsign))
	fmt.Fprintln(w, "START-OF-LOG: 3.0")
	fmt.Fprintln(w, "CREATED-BY: OpsLog")
	fmt.Fprintf(w, "CONTEST: %s\n", strings.ToUpper(def(h.Contest, "UNKNOWN")))
	fmt.Fprintf(w, "CALLSIGN: %s\n", myCall)
	fmt.Fprintf(w, "CATEGORY-OPERATOR: %s\n", strings.ToUpper(def(h.CatOperator, "SINGLE-OP")))
	fmt.Fprintf(w, "CATEGORY-BAND: %s\n", strings.ToUpper(def(h.CatBand, "ALL")))
	fmt.Fprintf(w, "CATEGORY-MODE: %s\n", strings.ToUpper(def(h.CatMode, "MIXED")))
	fmt.Fprintf(w, "CATEGORY-POWER: %s\n", strings.ToUpper(def(h.CatPower, "HIGH")))
	fmt.Fprintln(w, "CLAIMED-SCORE: 0")
	fmt.Fprintf(w, "OPERATORS: %s\n", strings.ToUpper(def(h.Operators, myCall)))
	if h.GridLocator != "" {
		fmt.Fprintf(w, "GRID-LOCATOR: %s\n", strings.ToUpper(h.GridLocator))
	}
	if h.Name != "" {
		fmt.Fprintf(w, "NAME: %s\n", h.Name)
	}
	if h.Club != "" {
		fmt.Fprintf(w, "CLUB: %s\n", h.Club)
	}
	if h.Email != "" {
		fmt.Fprintf(w, "EMAIL: %s\n", h.Email)
	}

	n := 0
	for _, q := range qsos {
		call := strings.ToUpper(strings.TrimSpace(q.Callsign))
		if call == "" {
			continue
		}
		khz := freqKHz(q)
		mode := cabrilloMode(q.Mode)
		myRST := def(q.RSTSent, defaultRST(mode))
		hisRST := def(q.RSTRcvd, defaultRST(mode))
		myExch := exchange(q.STX, q.STXString)
		hisExch := exchange(q.SRX, q.SRXString)
		// QSO: <freq> <mo> <date> <time> <mycall> <myrst> <myexch> <call> <rst> <exch>
		fmt.Fprintf(w, "QSO: %5d %-2s %s %s %-13s %-3s %-6s %-13s %-3s %-6s\n",
			khz, mode,
			q.QSODate.UTC().Format("2006-01-02"), q.QSODate.UTC().Format("1504"),
			myCall, myRST, myExch, call, hisRST, hisExch)
		n++
	}
	fmt.Fprintln(w, "END-OF-LOG:")
	return n, nil
}

// freqKHz returns the QSO frequency in kHz, falling back to a nominal frequency
// for the band when the exact frequency wasn't logged.
func freqKHz(q qso.QSO) int {
	if q.FreqHz != nil && *q.FreqHz > 0 {
		return int((*q.FreqHz + 500) / 1000)
	}
	return bandNominalKHz(q.Band)
}

func bandNominalKHz(band string) int {
	switch strings.ToLower(strings.TrimSpace(band)) {
	case "160m":
		return 1800
	case "80m":
		return 3500
	case "60m":
		return 5350
	case "40m":
		return 7000
	case "30m":
		return 10100
	case "20m":
		return 14000
	case "17m":
		return 18068
	case "15m":
		return 21000
	case "12m":
		return 24890
	case "10m":
		return 28000
	case "6m":
		return 50000
	case "2m":
		return 144000
	case "70cm":
		return 432000
	}
	return 0
}

// cabrilloMode maps an ADIF mode to the two-letter Cabrillo mode code.
func cabrilloMode(mode string) string {
	switch strings.ToUpper(strings.TrimSpace(mode)) {
	case "CW":
		return "CW"
	case "RTTY":
		return "RY"
	case "FM":
		return "FM"
	case "SSB", "USB", "LSB", "AM":
		return "PH"
	case "":
		return "PH"
	default:
		return "DG" // FT8/FT4/PSK/JT/DATA and other digital modes
	}
}

func defaultRST(cabMode string) string {
	if cabMode == "CW" || cabMode == "RY" || cabMode == "DG" {
		return "599"
	}
	return "59"
}

// exchange renders the contest exchange: the string field if set, else the
// numeric serial, else empty.
func exchange(num *int, str string) string {
	if s := strings.TrimSpace(str); s != "" {
		return s
	}
	if num != nil && *num > 0 {
		return fmt.Sprintf("%d", *num)
	}
	return ""
}
