// Package extsvc uploads logged QSOs to external logbook services
// (QRZ.com first; Clublog and LoTW to follow). Each service has its own
// credentials and an upload mode chosen per-service: "immediate" pushes as
// soon as the QSO is saved, "delayed" waits a random 1–2 minutes (like
// Log4OM) so a mistakenly-logged QSO can still be edited or removed before
// it leaves.
//
// The Manager is intentionally fire-and-forget: a failed or skipped upload
// just leaves the QSO's per-service upload-status column empty, and the
// (future) manual-upload window will let the user retry the backlog.
package extsvc

import (
	"errors"
	"strings"
)

// errFromResult turns a non-OK result with no transport error into one
// (defensive — uploaders normally return an error alongside !OK).
func errFromResult(r UploadResult) error {
	if r.Message != "" {
		return errors.New(r.Message)
	}
	return errors.New("upload rejected")
}

// Service identifies one external logbook.
type Service string

const (
	ServiceQRZ     Service = "qrz"     // QRZ.com Logbook
	ServiceClublog Service = "clublog" // Club Log real-time upload
	ServiceLoTW    Service = "lotw"    // ARRL Logbook of The World (via TQSL)
	ServiceHRDLog  Service = "hrdlog"  // HRDLog.net real-time upload
	ServiceEQSL    Service = "eqsl"    // eQSL.cc ADIF upload
	// ServiceCloudlog covers Cloudlog AND its fork Wavelog: same API contract,
	// only the instance URL differs, so one service handles both.
	ServiceCloudlog Service = "cloudlog"
	// ServiceHamlog is HAMLOG.online — one API key, an ADIF record per QSO.
	ServiceHamlog Service = "hamlog"
	// ServiceHamQTH is the HamQTH online logbook — the callbook credentials,
	// one ADIF record per QSO.
	ServiceHamQTH Service = "hamqth"
)

// UploadMode selects when an auto-upload fires after a QSO is saved.
type UploadMode string

const (
	// ModeImmediate uploads as soon as the QSO is logged.
	ModeImmediate UploadMode = "immediate"
	// ModeDelayed waits a random 1–2 minutes before uploading.
	ModeDelayed UploadMode = "delayed"
	// ModeOnClose queues QSOs and uploads them in one batch when the app
	// closes. This is the LoTW-friendly mode (ARRL discourages per-QSO
	// uploads), and it lets the user fix the whole session before sending.
	ModeOnClose UploadMode = "on_close"
)

// ServiceConfig is the per-service user configuration. It's a superset of
// the credential shapes the different services need — each service reads
// only the fields it uses:
//
//	QRZ.com  → APIKey, ForceStationCallsign
//	Club Log → Email, Password, Callsign, APIKey
//	LoTW     → TQSLPath, StationLocation, ForceStationCallsign, KeyPassword
//	           (signs+uploads via TQSL; ForceStationCallsign overrides
//	           STATION_CALLSIGN so one cert can sign F4BPO / F4BPO/P / TM2Q)
//
// AutoUpload + UploadMode are common to all (timing is per-service, so the
// user can run e.g. Club Log immediate and QRZ delayed).
type ServiceConfig struct {
	APIKey               string   `json:"api_key"`
	URL                  string   `json:"url"`                    // Cloudlog/Wavelog: base URL of the user's own instance
	StationID            string   `json:"station_id"`             // Cloudlog/Wavelog: station profile (location) id
	Email                string   `json:"email"`                  // Club Log account email
	Username             string   `json:"username"`               // LoTW website login (for confirmation download)
	Password             string   `json:"password"`               // Club Log account / LoTW website password
	Callsign             string   `json:"callsign"`               // Club Log / HRDLog logbook (owner) callsign
	Code                 string   `json:"code"`                   // HRDLog: account upload code
	QTHNickname          string   `json:"qth_nickname"`           // eQSL: QTH nickname (when the account has several)
	ForceStationCallsign string   `json:"force_station_callsign"` // QRZ + LoTW: override STATION_CALLSIGN
	TQSLPath             string   `json:"tqsl_path"`              // LoTW: path to tqsl.exe
	StationLocation      string   `json:"station_location"`       // LoTW: TQSL Station Location name
	KeyPassword          string   `json:"key_password"`           // LoTW: certificate private-key password (optional)
	UploadFlags          []string `json:"upload_flags"`           // LoTW: set of lotw_sent values that mean "ready to upload" — any of "N"/"R"
	WriteLog             bool     `json:"write_log"`              // LoTW: pass -t to write a TQSL diagnostic log
	AutoUpload           bool     `json:"auto_upload"`
	// OnAir: HRDLog only — publish the live frequency, mode and rig on the site.
	OnAir      bool       `json:"on_air"`
	UploadMode UploadMode `json:"upload_mode"`
}

// normalised returns the config with whitespace trimmed and a valid upload
// mode (defaults to immediate).
func (c ServiceConfig) normalised() ServiceConfig {
	c.APIKey = strings.TrimSpace(c.APIKey)
	c.URL = strings.TrimSpace(c.URL)
	c.StationID = strings.TrimSpace(c.StationID)
	c.Email = strings.TrimSpace(c.Email)
	c.Callsign = strings.ToUpper(strings.TrimSpace(c.Callsign))
	c.Code = strings.TrimSpace(c.Code)
	c.Username = strings.TrimSpace(c.Username)
	c.QTHNickname = strings.TrimSpace(c.QTHNickname)
	c.ForceStationCallsign = strings.ToUpper(strings.TrimSpace(c.ForceStationCallsign))
	c.TQSLPath = strings.TrimSpace(c.TQSLPath)
	c.StationLocation = strings.TrimSpace(c.StationLocation)
	// Upload flags are the LoTW sent-statuses that mark a QSO ready to upload.
	// Only "N" (no) and "R" (requested) are valid; clean + dedupe, no default
	// here (the caller injects N+R when nothing is configured).
	var flags []string
	seen := map[string]bool{}
	for _, f := range c.UploadFlags {
		f = strings.ToUpper(strings.TrimSpace(f))
		if (f == "N" || f == "R") && !seen[f] {
			seen[f] = true
			flags = append(flags, f)
		}
	}
	c.UploadFlags = flags
	switch c.UploadMode {
	case ModeDelayed, ModeOnClose:
		// keep
	default:
		c.UploadMode = ModeImmediate
	}
	return c
}

// ExternalServices bundles every service's config for the settings UI.
type ExternalServices struct {
	QRZ      ServiceConfig `json:"qrz"`
	Clublog  ServiceConfig `json:"clublog"`
	LoTW     ServiceConfig `json:"lotw"`
	HRDLog   ServiceConfig `json:"hrdlog"`
	EQSL     ServiceConfig `json:"eqsl"`
	Cloudlog ServiceConfig `json:"cloudlog"`
	Hamlog   ServiceConfig `json:"hamlog"`
	HamQTH   ServiceConfig `json:"hamqth"`

	// DeleteRemote asks OpsLog to withdraw a QSO from QRZ.com and Club Log when
	// it is deleted locally. Off unless the operator turns it on: neither
	// service can undo it, and a local delete is not always meant to reach the
	// world — a duplicate cleared from a contest log was never meant to be a
	// statement about the DX's log.
	DeleteRemote bool `json:"delete_remote"`
}

// UploadResult is the outcome of a single upload attempt.
type UploadResult struct {
	OK      bool   // the service accepted (or already had) the QSO
	LogID   string // service-assigned record id, when provided
	Message string // human-readable detail (reason on failure)
	// Ignored is set when the service accepted the submission but left some or
	// all of the QSOs out of it. TQSL does this for contacts already uploaded
	// AND for contacts outside the certificate's date range, and reports both
	// with the same exit code — so the caller must show the message rather than
	// decide on its own that everything went through.
	Ignored bool
}
