// Package profile manages operator profiles: transmit callsign, locator,
// DXCC, operator, antenna/CAT config, etc. Lets the user switch quickly
// between several identities (home / portable / SOTA …).
//
// The active profile stamps every new QSO's MY_* fields (MY_GRIDSQUARE,
// MY_SOTA_REF, MY_RIG…). Future versions will also attach per-profile
// credentials (LoTW / Clublog / QRZ.com) so each callsign can export to
// its own account.
package profile

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

// ProfileDB is a profile's logbook database target. Backend "" or "sqlite" =
// the local SQLite file; "mysql" = the shared MySQL server described by the
// rest. Switching the active profile switches the live logbook to this.
type ProfileDB struct {
	Backend  string `json:"backend"`
	Host     string `json:"host"`
	Port     int    `json:"port"`
	User     string `json:"user"`
	Password string `json:"password"`
	Database string `json:"database"`
	// Path is a per-profile SQLite logbook FILE, distinct from the shared app
	// database (opslog.db, which always holds settings + profiles). Empty =
	// the shared database is used as the logbook (the historical default). Set =
	// this profile's QSOs live in their own .db, so a visiting operator's contacts
	// don't mix into yours and switching it never touches your config. Only used
	// when Backend != "mysql".
	Path string `json:"path,omitempty"`
}

// Profile is one operating configuration. A user typically keeps a few:
// "Home", "Portable", "SOTA Pic du Midi", "/MM cruise"…
type Profile struct {
	ID            int64     `json:"id"`
	Name          string    `json:"name"`
	Callsign      string    `json:"callsign"`
	Operator      string    `json:"operator"`
	OpName        string    `json:"op_name"` // operator's personal name (e.g. "Greg")
	OwnerCallsign string    `json:"owner_callsign"`
	MyGrid        string    `json:"my_grid"`
	MyCountry     string    `json:"my_country"`
	MyState       string    `json:"my_state"`
	MyCounty      string    `json:"my_cnty"`
	MyStreet      string    `json:"my_street"`
	MyCity        string    `json:"my_city"`
	MyPostalCode  string    `json:"my_postal_code"`
	MySOTARef     string    `json:"my_sota_ref"`
	MyIOTA        string    `json:"my_iota"`
	MyPOTARef     string    `json:"my_pota_ref"`
	MyRig         string    `json:"my_rig"`
	MyAntenna     string    `json:"my_antenna"`
	MyDXCC        *int      `json:"my_dxcc,omitempty"`
	MyCQZone      *int      `json:"my_cqz,omitempty"`
	MyITUZone     *int      `json:"my_ituz,omitempty"`
	MyLat         *float64  `json:"my_lat,omitempty"`
	MyLon         *float64  `json:"my_lon,omitempty"`
	TxPower       *float64  `json:"tx_pwr,omitempty"`
	IsActive      bool      `json:"is_active"`
	SortOrder     int       `json:"sort_order"`
	DB            ProfileDB `json:"db"` // per-profile logbook target (empty backend = local SQLite)
	CreatedAt     time.Time `json:"created_at"`
	UpdatedAt     time.Time `json:"updated_at"`
}

// Repo is a SQLite-backed profile store. All ops take a context so the
// HTTP/Wails frontend can cancel them on tab close.
type Repo struct{ db *sql.DB }

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

const selectCols = `id, name, callsign, operator, op_name, owner_callsign, my_grid, my_country, my_state, my_cnty,
    my_street, my_city, my_postal_code, my_sota_ref, my_pota_ref, my_iota,
    my_rig, my_antenna, my_dxcc, my_cqz, my_ituz, my_lat, my_lon, tx_pwr,
    is_active, sort_order, db_config, created_at, updated_at`

// List returns every profile, active first then by sort_order/id.
func (r *Repo) List(ctx context.Context) ([]Profile, error) {
	rows, err := r.db.QueryContext(ctx, `SELECT `+selectCols+`
		FROM station_profiles
		ORDER BY is_active DESC, sort_order ASC, id ASC`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []Profile
	for rows.Next() {
		p, err := scan(rows)
		if err != nil {
			return nil, err
		}
		out = append(out, p)
	}
	return out, rows.Err()
}

// Get returns one profile by ID, or sql.ErrNoRows if missing.
func (r *Repo) Get(ctx context.Context, id int64) (Profile, error) {
	row := r.db.QueryRowContext(ctx, `SELECT `+selectCols+`
		FROM station_profiles WHERE id = ?`, id)
	return scan(row)
}

// Active returns the currently-active profile, or sql.ErrNoRows if none.
func (r *Repo) Active(ctx context.Context) (Profile, error) {
	row := r.db.QueryRowContext(ctx, `SELECT `+selectCols+`
		FROM station_profiles WHERE is_active = 1 LIMIT 1`)
	return scan(row)
}

// Save upserts a profile. p.ID == 0 means "create". Updates touch
// updated_at; is_active is preserved separately via SetActive.
func (r *Repo) Save(ctx context.Context, p *Profile) error {
	if p.Name == "" {
		return fmt.Errorf("profile name required")
	}
	now := time.Now().UTC().Format("2006-01-02T15:04:05.000Z")
	if p.ID == 0 {
		res, err := r.db.ExecContext(ctx, `
			INSERT INTO station_profiles
			    (name, callsign, operator, op_name, owner_callsign, my_grid, my_country, my_state, my_cnty,
			     my_street, my_city, my_postal_code, my_sota_ref, my_pota_ref, my_iota,
			     my_rig, my_antenna, my_dxcc, my_cqz, my_ituz, my_lat, my_lon, tx_pwr,
			     is_active, sort_order, created_at, updated_at)
			VALUES(?,?,?,?,?,?,?,?,?, ?,?,?,?,?,?, ?,?,?,?,?,?,?,?, ?,?,?,?)`,
			p.Name, p.Callsign, p.Operator, p.OpName, p.OwnerCallsign, p.MyGrid, p.MyCountry, p.MyState, p.MyCounty,
			p.MyStreet, p.MyCity, p.MyPostalCode, p.MySOTARef, p.MyPOTARef, p.MyIOTA,
			p.MyRig, p.MyAntenna, nullableInt(p.MyDXCC), nullableInt(p.MyCQZone), nullableInt(p.MyITUZone),
			nullableFloat(p.MyLat), nullableFloat(p.MyLon), nullableFloat(p.TxPower),
			boolInt(p.IsActive), p.SortOrder, now, now)
		if err != nil {
			return err
		}
		id, _ := res.LastInsertId()
		p.ID = id
		return nil
	}
	_, err := r.db.ExecContext(ctx, `
		UPDATE station_profiles SET
		    name = ?, callsign = ?, operator = ?, op_name = ?, owner_callsign = ?, my_grid = ?, my_country = ?,
		    my_state = ?, my_cnty = ?, my_street = ?, my_city = ?, my_postal_code = ?,
		    my_sota_ref = ?, my_pota_ref = ?, my_iota = ?, my_rig = ?, my_antenna = ?,
		    my_dxcc = ?, my_cqz = ?, my_ituz = ?, my_lat = ?, my_lon = ?, tx_pwr = ?,
		    sort_order = ?, updated_at = ?
		WHERE id = ?`,
		p.Name, p.Callsign, p.Operator, p.OpName, p.OwnerCallsign, p.MyGrid, p.MyCountry,
		p.MyState, p.MyCounty, p.MyStreet, p.MyCity, p.MyPostalCode,
		p.MySOTARef, p.MyPOTARef, p.MyIOTA, p.MyRig, p.MyAntenna,
		nullableInt(p.MyDXCC), nullableInt(p.MyCQZone), nullableInt(p.MyITUZone),
		nullableFloat(p.MyLat), nullableFloat(p.MyLon), nullableFloat(p.TxPower),
		p.SortOrder, now, p.ID)
	return err
}

// SetDB updates only a profile's logbook database target, leaving the rest of
// the profile untouched (the general Save deliberately doesn't write db_config).
func (r *Repo) SetDB(ctx context.Context, id int64, cfg ProfileDB) error {
	b, err := json.Marshal(cfg)
	if err != nil {
		return err
	}
	now := time.Now().UTC().Format("2006-01-02T15:04:05.000Z")
	_, err = r.db.ExecContext(ctx,
		`UPDATE station_profiles SET db_config = ?, updated_at = ? WHERE id = ?`,
		string(b), now, id)
	return err
}

// SetActive atomically switches the active profile. Clears the flag on all
// rows first to keep the "only one active" invariant from the schema doc.
func (r *Repo) SetActive(ctx context.Context, id int64) error {
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer tx.Rollback()
	if _, err := tx.ExecContext(ctx, `UPDATE station_profiles SET is_active = 0`); err != nil {
		return err
	}
	res, err := tx.ExecContext(ctx, `UPDATE station_profiles SET is_active = 1 WHERE id = ?`, id)
	if err != nil {
		return err
	}
	n, _ := res.RowsAffected()
	if n == 0 {
		return sql.ErrNoRows
	}
	return tx.Commit()
}

// Delete removes a profile. Refuses to delete the last remaining profile
// (we always want at least one so QSO stamping doesn't crash). If the
// deleted one was active, the first remaining profile becomes active.
func (r *Repo) Delete(ctx context.Context, id int64) error {
	var count int
	if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM station_profiles`).Scan(&count); err != nil {
		return err
	}
	if count <= 1 {
		return fmt.Errorf("cannot delete the last remaining profile")
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer tx.Rollback()
	var wasActive int
	if err := tx.QueryRowContext(ctx, `SELECT is_active FROM station_profiles WHERE id = ?`, id).Scan(&wasActive); err != nil {
		return err
	}
	if _, err := tx.ExecContext(ctx, `DELETE FROM station_profiles WHERE id = ?`, id); err != nil {
		return err
	}
	if wasActive == 1 {
		// Promote the first remaining profile.
		var newID int64
		if err := tx.QueryRowContext(ctx,
			`SELECT id FROM station_profiles ORDER BY sort_order ASC, id ASC LIMIT 1`).Scan(&newID); err != nil {
			return err
		}
		if _, err := tx.ExecContext(ctx, `UPDATE station_profiles SET is_active = 1 WHERE id = ?`, newID); err != nil {
			return err
		}
	}
	return tx.Commit()
}

// Duplicate clones a profile under a new name (caller supplies it). The
// copy is created inactive — switching is an explicit user action.
func (r *Repo) Duplicate(ctx context.Context, srcID int64, newName string) (Profile, error) {
	src, err := r.Get(ctx, srcID)
	if err != nil {
		return Profile{}, err
	}
	dbCfg := src.DB // Save deliberately doesn't write db_config — copy it after.
	src.ID = 0
	src.Name = newName
	src.IsActive = false
	src.SortOrder = 0
	if err := r.Save(ctx, &src); err != nil {
		return Profile{}, err
	}
	if dbCfg.Backend != "" {
		if err := r.SetDB(ctx, src.ID, dbCfg); err != nil {
			return Profile{}, err
		}
		src.DB = dbCfg
	}
	return src, nil
}

// ----- helpers -----

type scannable interface {
	Scan(dest ...any) error
}

func scan(row scannable) (Profile, error) {
	var p Profile
	var (
		callsign, operator, opName, ownerCall, myGrid, myCountry, myState, myCnty,
		myStreet, myCity, myPostal, mySOTA, myPOTA, myIOTA,
		myRig, myAntenna sql.NullString
		myDXCC, myCQZ, myITUZ sql.NullInt64
		myLat, myLon, txPwr   sql.NullFloat64
		isActive, sortOrder   int
		dbConfig              sql.NullString
		createdAt, updatedAt  string
	)
	err := row.Scan(&p.ID, &p.Name, &callsign, &operator, &opName, &ownerCall, &myGrid, &myCountry, &myState, &myCnty,
		&myStreet, &myCity, &myPostal, &mySOTA, &myPOTA, &myIOTA,
		&myRig, &myAntenna, &myDXCC, &myCQZ, &myITUZ, &myLat, &myLon, &txPwr,
		&isActive, &sortOrder, &dbConfig, &createdAt, &updatedAt)
	if err != nil {
		return p, err
	}
	if s := dbConfig.String; s != "" {
		_ = json.Unmarshal([]byte(s), &p.DB)
	}
	p.Callsign = callsign.String
	p.Operator = operator.String
	p.OpName = opName.String
	p.OwnerCallsign = ownerCall.String
	p.MyGrid = myGrid.String
	p.MyCountry = myCountry.String
	p.MyState = myState.String
	p.MyCounty = myCnty.String
	p.MyStreet = myStreet.String
	p.MyCity = myCity.String
	p.MyPostalCode = myPostal.String
	p.MySOTARef = mySOTA.String
	p.MyIOTA = myIOTA.String
	p.MyPOTARef = myPOTA.String
	p.MyRig = myRig.String
	p.MyAntenna = myAntenna.String
	if myDXCC.Valid {
		v := int(myDXCC.Int64)
		p.MyDXCC = &v
	}
	if myCQZ.Valid {
		v := int(myCQZ.Int64)
		p.MyCQZone = &v
	}
	if myITUZ.Valid {
		v := int(myITUZ.Int64)
		p.MyITUZone = &v
	}
	if myLat.Valid {
		v := myLat.Float64
		p.MyLat = &v
	}
	if myLon.Valid {
		v := myLon.Float64
		p.MyLon = &v
	}
	if txPwr.Valid {
		v := txPwr.Float64
		p.TxPower = &v
	}
	p.IsActive = isActive == 1
	p.SortOrder = sortOrder
	p.CreatedAt, _ = time.Parse("2006-01-02T15:04:05.000Z", createdAt)
	p.UpdatedAt, _ = time.Parse("2006-01-02T15:04:05.000Z", updatedAt)
	return p, nil
}

func nullableFloat(p *float64) any {
	if p == nil {
		return nil
	}
	return *p
}
func nullableInt(p *int) any {
	if p == nil {
		return nil
	}
	return *p
}
func boolInt(b bool) int {
	if b {
		return 1
	}
	return 0
}
