// Package operating manages the per-profile tree of stations (radios),
// antennas, and the bands each antenna covers. The "default for a band"
// flag drives the auto-fill of MY_RIG / MY_ANTENNA on each logged QSO.
package operating

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

	"hamlog/internal/db"
)

// Station is a radio / TRX line. The display Name is also what gets
// written into the MY_RIG ADIF field on each QSO — no separate ADIF
// value to maintain. TXPower (W) is per-rig so changing rig auto-
// stamps the right power on logged QSOs.
type Station struct {
	ID        int64     `json:"id"`
	ProfileID int64     `json:"profile_id"`
	Name      string    `json:"name"`
	TXPower   *float64  `json:"tx_pwr,omitempty"`
	SortOrder int       `json:"sort_order"`
	Antennas  []Antenna `json:"antennas,omitempty"`
}

// Antenna is one antenna attached to a station. The display Name doubles
// as the MY_ANTENNA ADIF value.
type Antenna struct {
	ID        int64         `json:"id"`
	StationID int64         `json:"station_id"`
	Name      string        `json:"name"`
	SortOrder int           `json:"sort_order"`
	Bands     []AntennaBand `json:"bands"`
}

// AntennaBand pairs an antenna with one of the bands it covers, plus
// whether it is the default for that band in this profile.
type AntennaBand struct {
	Band      string `json:"band"`
	IsDefault bool   `json:"is_default"`
}

// BandDefault is the resolved tuple looked up at QSO save: which
// station+antenna should pre-fill MY_RIG / MY_ANTENNA / TX_PWR for a
// given band. Station and antenna names go straight into the ADIF
// fields — there is no separate "ADIF value" anymore.
type BandDefault struct {
	StationID   int64    `json:"station_id"`
	StationName string   `json:"station_name"`
	AntennaID   int64    `json:"antenna_id"`
	AntennaName string   `json:"antenna_name"`
	TXPower     *float64 `json:"tx_pwr,omitempty"`
}

type Repo struct{ db *sql.DB }

func NewRepo(db *sql.DB) *Repo { return &Repo{db: db} }

// CopyProfile duplicates the entire stations→antennas→bands tree from one
// profile to another (used when a profile is duplicated). New ids are assigned;
// the band defaults and ordering are preserved.
func (r *Repo) CopyProfile(ctx context.Context, srcProfileID, dstProfileID int64) error {
	tree, err := r.ListTree(ctx, srcProfileID)
	if err != nil {
		return err
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer tx.Rollback()
	now := db.NowISO()
	for _, st := range tree {
		var pwr any
		if st.TXPower != nil {
			pwr = *st.TXPower
		}
		res, err := tx.ExecContext(ctx,
			`INSERT INTO operating_stations(profile_id, name, tx_pwr, sort_order, created_at, updated_at)
			 VALUES(?,?,?,?,?,?)`, dstProfileID, st.Name, pwr, st.SortOrder, now, now)
		if err != nil {
			return fmt.Errorf("copy station: %w", err)
		}
		newStationID, _ := res.LastInsertId()
		for _, ant := range st.Antennas {
			ares, err := tx.ExecContext(ctx,
				`INSERT INTO operating_antennas(station_id, name, sort_order, created_at, updated_at)
				 VALUES(?,?,?,?,?)`, newStationID, ant.Name, ant.SortOrder, now, now)
			if err != nil {
				return fmt.Errorf("copy antenna: %w", err)
			}
			newAntID, _ := ares.LastInsertId()
			for _, b := range ant.Bands {
				if _, err := tx.ExecContext(ctx,
					`INSERT INTO operating_antenna_bands(antenna_id, band, is_default) VALUES(?,?,?)`,
					newAntID, b.Band, boolToInt(b.IsDefault)); err != nil {
					return fmt.Errorf("copy band: %w", err)
				}
			}
		}
	}
	return tx.Commit()
}

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

// ListTree returns every station for the profile with its nested antennas
// and bands. One round-trip per level — three queries total regardless of
// tree size, so the Settings panel stays snappy on big setups.
func (r *Repo) ListTree(ctx context.Context, profileID int64) ([]Station, error) {
	rows, err := r.db.QueryContext(ctx,
		`SELECT id, profile_id, name, tx_pwr, sort_order
		 FROM operating_stations
		 WHERE profile_id = ?
		 ORDER BY sort_order, id`, profileID)
	if err != nil {
		return nil, fmt.Errorf("list stations: %w", err)
	}
	defer rows.Close()
	var stations []Station
	stationByID := map[int64]int{} // id → index in stations slice
	for rows.Next() {
		var s Station
		var pwr sql.NullFloat64
		if err := rows.Scan(&s.ID, &s.ProfileID, &s.Name, &pwr, &s.SortOrder); err != nil {
			return nil, err
		}
		if pwr.Valid {
			v := pwr.Float64
			s.TXPower = &v
		}
		stationByID[s.ID] = len(stations)
		stations = append(stations, s)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	if len(stations) == 0 {
		return stations, nil
	}

	// Build IN-clause placeholders for the second query.
	ids := make([]any, 0, len(stations))
	placeholders := make([]string, 0, len(stations))
	for _, s := range stations {
		ids = append(ids, s.ID)
		placeholders = append(placeholders, "?")
	}
	antRows, err := r.db.QueryContext(ctx,
		`SELECT id, station_id, name, sort_order
		 FROM operating_antennas
		 WHERE station_id IN (`+strings.Join(placeholders, ",")+`)
		 ORDER BY station_id, sort_order, id`, ids...)
	if err != nil {
		return nil, fmt.Errorf("list antennas: %w", err)
	}
	// Collect antennas into a flat map keyed by ID first — taking pointers
	// into a slice we later append to is unsafe (a re-allocation
	// invalidates older pointers, leaving the band loop writing to dead
	// memory). We assemble the per-station slices at the very end, once
	// everything is collected.
	antennasByID := map[int64]*Antenna{}
	antennaIDsByStation := map[int64][]int64{}
	for antRows.Next() {
		a := &Antenna{}
		if err := antRows.Scan(&a.ID, &a.StationID, &a.Name, &a.SortOrder); err != nil {
			antRows.Close()
			return nil, err
		}
		antennasByID[a.ID] = a
		antennaIDsByStation[a.StationID] = append(antennaIDsByStation[a.StationID], a.ID)
	}
	antRows.Close()

	if len(antennasByID) > 0 {
		antIDs := make([]any, 0, len(antennasByID))
		antPlaceholders := make([]string, 0, len(antennasByID))
		for id := range antennasByID {
			antIDs = append(antIDs, id)
			antPlaceholders = append(antPlaceholders, "?")
		}
		bandRows, err := r.db.QueryContext(ctx,
			`SELECT antenna_id, band, is_default
			 FROM operating_antenna_bands
			 WHERE antenna_id IN (`+strings.Join(antPlaceholders, ",")+`)
			 ORDER BY band`, antIDs...)
		if err != nil {
			return nil, fmt.Errorf("list bands: %w", err)
		}
		for bandRows.Next() {
			var antID int64
			var band string
			var isDefault int
			if err := bandRows.Scan(&antID, &band, &isDefault); err != nil {
				bandRows.Close()
				return nil, err
			}
			if a, ok := antennasByID[antID]; ok {
				a.Bands = append(a.Bands, AntennaBand{Band: band, IsDefault: isDefault != 0})
			}
		}
		bandRows.Close()
	}

	// Now assemble each station's Antennas slice. By the time we do this
	// every antenna already has its full band list attached, so no
	// downstream pointer is left behind.
	for sIdx := range stations {
		for _, antID := range antennaIDsByStation[stations[sIdx].ID] {
			if a, ok := antennasByID[antID]; ok {
				stations[sIdx].Antennas = append(stations[sIdx].Antennas, *a)
			}
		}
	}
	return stations, nil
}

// SaveStation upserts a station. Returns the (possibly new) ID.
func (r *Repo) SaveStation(ctx context.Context, s *Station) error {
	if strings.TrimSpace(s.Name) == "" {
		return fmt.Errorf("station name required")
	}
	var pwr any
	if s.TXPower != nil {
		pwr = *s.TXPower
	}
	if s.ID == 0 {
		res, err := r.db.ExecContext(ctx,
			`INSERT INTO operating_stations(profile_id, name, tx_pwr, sort_order)
			 VALUES(?, ?, ?, ?)`, s.ProfileID, s.Name, pwr, s.SortOrder)
		if err != nil {
			return fmt.Errorf("insert station: %w", err)
		}
		id, _ := res.LastInsertId()
		s.ID = id
		return nil
	}
	_, err := r.db.ExecContext(ctx,
		`UPDATE operating_stations
		 SET name = ?, tx_pwr = ?, sort_order = ?,
		     updated_at = ?
		 WHERE id = ?`, s.Name, pwr, s.SortOrder, db.NowISO(), s.ID)
	if err != nil {
		return fmt.Errorf("update station: %w", err)
	}
	return nil
}

// DeleteStation cascades to antennas and bands via FK ON DELETE CASCADE.
func (r *Repo) DeleteStation(ctx context.Context, id int64) error {
	_, err := r.db.ExecContext(ctx, `DELETE FROM operating_stations WHERE id = ?`, id)
	return err
}

// SaveAntenna upserts an antenna and replaces its band list in one
// transaction. `is_default` is enforced per profile: setting it on one
// antenna clears any other antenna's default for the same band.
func (r *Repo) SaveAntenna(ctx context.Context, a *Antenna) error {
	if strings.TrimSpace(a.Name) == "" {
		return fmt.Errorf("antenna name required")
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer tx.Rollback()

	if a.ID == 0 {
		res, err := tx.ExecContext(ctx,
			`INSERT INTO operating_antennas(station_id, name, sort_order)
			 VALUES(?, ?, ?)`, a.StationID, a.Name, a.SortOrder)
		if err != nil {
			return fmt.Errorf("insert antenna: %w", err)
		}
		id, _ := res.LastInsertId()
		a.ID = id
	} else {
		if _, err := tx.ExecContext(ctx,
			`UPDATE operating_antennas
			 SET name = ?, sort_order = ?,
			     updated_at = ?
			 WHERE id = ?`, a.Name, a.SortOrder, db.NowISO(), a.ID); err != nil {
			return fmt.Errorf("update antenna: %w", err)
		}
	}

	// Look up profile_id for this antenna's station — needed for the
	// "single default per band per profile" constraint.
	var profileID int64
	if err := tx.QueryRowContext(ctx,
		`SELECT s.profile_id FROM operating_stations s WHERE s.id = ?`,
		a.StationID).Scan(&profileID); err != nil {
		return fmt.Errorf("lookup profile id: %w", err)
	}

	// Replace band list wholesale — simpler than diffing, fine for the
	// small N (a typical antenna covers a handful of bands).
	if _, err := tx.ExecContext(ctx,
		`DELETE FROM operating_antenna_bands WHERE antenna_id = ?`, a.ID); err != nil {
		return fmt.Errorf("clear bands: %w", err)
	}
	for _, b := range a.Bands {
		band := strings.TrimSpace(strings.ToLower(b.Band))
		if band == "" {
			continue
		}
		def := 0
		if b.IsDefault {
			def = 1
		}
		// Insert this antenna's band entry, then if it's a default
		// clear other antennas' default for the same band within
		// the same profile.
		if _, err := tx.ExecContext(ctx,
			`INSERT INTO operating_antenna_bands(antenna_id, band, is_default)
			 VALUES(?, ?, ?)`, a.ID, band, def); err != nil {
			return fmt.Errorf("insert band: %w", err)
		}
		if def == 1 {
			if _, err := tx.ExecContext(ctx, `
				UPDATE operating_antenna_bands
				SET is_default = 0
				WHERE band = ?
				  AND antenna_id != ?
				  AND antenna_id IN (
				      SELECT oa.id FROM operating_antennas oa
				      JOIN operating_stations os ON oa.station_id = os.id
				      WHERE os.profile_id = ?
				  )`, band, a.ID, profileID); err != nil {
				return fmt.Errorf("clear other defaults: %w", err)
			}
		}
	}
	return tx.Commit()
}

// DeleteAntenna cascades to bands via FK ON DELETE CASCADE.
func (r *Repo) DeleteAntenna(ctx context.Context, id int64) error {
	_, err := r.db.ExecContext(ctx, `DELETE FROM operating_antennas WHERE id = ?`, id)
	return err
}

// BandDefault returns the (station, antenna) flagged default for the given
// band in the given profile. Empty result when nothing matches — callers
// should leave MY_RIG/MY_ANTENNA blank in that case.
func (r *Repo) BandDefault(ctx context.Context, profileID int64, band string) (BandDefault, bool, error) {
	band = strings.TrimSpace(strings.ToLower(band))
	if band == "" {
		return BandDefault{}, false, nil
	}
	row := r.db.QueryRowContext(ctx, `
		SELECT s.id, s.name, s.tx_pwr,
		       a.id, a.name
		FROM operating_antenna_bands ab
		JOIN operating_antennas a  ON ab.antenna_id = a.id
		JOIN operating_stations  s  ON a.station_id  = s.id
		WHERE s.profile_id = ? AND ab.band = ? AND ab.is_default = 1
		LIMIT 1`, profileID, band)
	var (
		d   BandDefault
		pwr sql.NullFloat64
	)
	if err := row.Scan(&d.StationID, &d.StationName, &pwr,
		&d.AntennaID, &d.AntennaName); err != nil {
		if err == sql.ErrNoRows {
			return BandDefault{}, false, nil
		}
		return BandDefault{}, false, err
	}
	if pwr.Valid {
		v := pwr.Float64
		d.TXPower = &v
	}
	return d, true, nil
}
