package extsvc

import (
	"context"
	"fmt"
	"html"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"
)

// qrzAPIURL is the QRZ.com Logbook API endpoint. Note this is the LOGBOOK
// API (key from the logbook's settings page), NOT the XML lookup
// subscription used elsewhere for callsign data — they're different keys.
const qrzAPIURL = "https://logbook.qrz.com/api"

// LogSink receives diagnostics such as raw QRZ responses. Set to applog.Printf
// at startup; defaults to a no-op so the package is usable without wiring.
var LogSink = func(string, ...any) {}

// UploadQRZ pushes one ADIF record to the QRZ.com logbook identified by
// apiKey. It returns OK when the QSO is inserted or already present
// (QRZ reports a duplicate as a FAIL with a "duplicate" reason, which we
// treat as success so retries are idempotent).
//
// API shape (form-encoded POST):
//
//	KEY=<logbook key>&ACTION=INSERT&ADIF=<one record>&OPTION=
//
// Response is URL-encoded key/values, e.g.:
//
//	STATUS=OK&LOGID=123456&COUNT=1&...
//	STATUS=FAIL&REASON=Unable+to+add+QSO+...&...
//	STATUS=AUTH&REASON=invalid+api+key&...
func UploadQRZ(ctx context.Context, client *http.Client, apiKey, adifRecord string) (UploadResult, error) {
	apiKey = strings.TrimSpace(apiKey)
	if apiKey == "" {
		return UploadResult{}, fmt.Errorf("qrz: api key not set")
	}
	if strings.TrimSpace(adifRecord) == "" {
		return UploadResult{}, fmt.Errorf("qrz: empty adif record")
	}

	form := url.Values{}
	form.Set("KEY", apiKey)
	form.Set("ACTION", "INSERT")
	// OPTION=REPLACE would overwrite an existing matching QSO; we leave it
	// empty so QRZ rejects duplicates (which we map to OK below).
	form.Set("ADIF", adifRecord)

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, qrzAPIURL, strings.NewReader(form.Encode()))
	if err != nil {
		return UploadResult{}, fmt.Errorf("qrz: build request: %w", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	if client == nil {
		client = &http.Client{Timeout: 20 * time.Second}
	}
	resp, err := client.Do(req)
	if err != nil {
		return UploadResult{}, fmt.Errorf("qrz: request failed: %w", err)
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
	if err != nil {
		return UploadResult{}, fmt.Errorf("qrz: read response: %w", err)
	}
	if resp.StatusCode != http.StatusOK {
		return UploadResult{}, fmt.Errorf("qrz: http %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
	}
	LogSink("qrz: INSERT raw response: %s", strings.TrimSpace(string(body)))

	return parseQRZResponse(string(body))
}

// QRZFetchResult is the parsed outcome of a QRZ FETCH.
type QRZFetchResult struct {
	ADIF   string // raw ADIF document
	Result string // RESULT field (OK / FAIL / AUTH)
	Count  string // COUNT field reported by QRZ
}

// FetchQRZ pulls logbook records as ADIF via the QRZ FETCH action. option is
// the QRZ OPTION string (e.g. "ALL"). The ADIF document is returned in the
// response's ADIF field.
func FetchQRZ(ctx context.Context, client *http.Client, apiKey, option string) (QRZFetchResult, error) {
	var out QRZFetchResult
	apiKey = strings.TrimSpace(apiKey)
	if apiKey == "" {
		return out, fmt.Errorf("qrz: api key not set")
	}
	form := url.Values{}
	form.Set("KEY", apiKey)
	form.Set("ACTION", "FETCH")
	if option != "" {
		form.Set("OPTION", option)
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, qrzAPIURL, strings.NewReader(form.Encode()))
	if err != nil {
		return out, fmt.Errorf("qrz: build request: %w", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	if client == nil {
		client = &http.Client{Timeout: 120 * time.Second}
	}
	resp, err := client.Do(req)
	if err != nil {
		return out, fmt.Errorf("qrz: request failed: %w", err)
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024*1024))
	if err != nil {
		return out, fmt.Errorf("qrz: read response: %w", err)
	}
	// The response is "RESULT=OK&COUNT=N&ADIF=<adif>". The ADIF blob can
	// contain '&' and ';', so we can't url.ParseQuery the whole body (Go
	// caps the number of params). Split off the ADIF value manually and
	// only query-parse the small status header.
	full := string(body)
	head, adifPart := full, ""
	if i := strings.Index(full, "ADIF="); i >= 0 {
		head = full[:i]
		adifPart = full[i+len("ADIF="):]
	}
	vals, _ := url.ParseQuery(strings.TrimRight(head, "&"))
	out.Result = strings.ToUpper(strings.TrimSpace(vals.Get("RESULT")))
	out.Count = strings.TrimSpace(vals.Get("COUNT"))
	if out.Result == "AUTH" || out.Result == "FAIL" {
		reason := strings.TrimSpace(vals.Get("REASON"))
		if reason == "" {
			reason = "fetch rejected"
		}
		return out, fmt.Errorf("qrz: %s", reason)
	}
	// The ADIF value may be url-encoded (%3C) and/or HTML-entity-encoded
	// (QRZ returns &lt; &gt; &amp;). Decode both so the ADIF parser sees
	// real '<' / '>' tags.
	if strings.Contains(adifPart, "%3C") || strings.Contains(adifPart, "%3c") {
		if dec, derr := url.QueryUnescape(adifPart); derr == nil {
			adifPart = dec
		}
	}
	if strings.Contains(adifPart, "&lt;") || strings.Contains(adifPart, "&gt;") || strings.Contains(adifPart, "&amp;") {
		adifPart = html.UnescapeString(adifPart)
	}
	out.ADIF = adifPart
	return out, nil
}

// TestQRZ checks a logbook API key with ACTION=STATUS and returns a short
// human-readable summary (callsign + QSO count) for the settings UI. An
// invalid key comes back as STATUS=AUTH → returned as an error.
func TestQRZ(ctx context.Context, client *http.Client, apiKey string) (string, error) {
	apiKey = strings.TrimSpace(apiKey)
	if apiKey == "" {
		return "", fmt.Errorf("qrz: api key not set")
	}
	form := url.Values{}
	form.Set("KEY", apiKey)
	form.Set("ACTION", "STATUS")

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, qrzAPIURL, strings.NewReader(form.Encode()))
	if err != nil {
		return "", fmt.Errorf("qrz: build request: %w", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	if client == nil {
		client = &http.Client{Timeout: 20 * time.Second}
	}
	resp, err := client.Do(req)
	if err != nil {
		return "", fmt.Errorf("qrz: request failed: %w", err)
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
	LogSink("qrz: STATUS raw response: %s", strings.TrimSpace(string(body)))
	vals, err := url.ParseQuery(strings.TrimSpace(string(body)))
	if err != nil {
		return "", fmt.Errorf("qrz: bad response: %w", err)
	}
	status := qrzStatusField(vals)
	if status == "AUTH" || status == "FAIL" {
		reason := strings.TrimSpace(vals.Get("REASON"))
		if reason == "" {
			reason = "invalid API key"
		}
		return "", fmt.Errorf("qrz: %s", reason)
	}
	call := strings.TrimSpace(vals.Get("CALLSIGN"))
	count := strings.TrimSpace(vals.Get("COUNT"))
	switch {
	case call != "" && count != "":
		return fmt.Sprintf("Connected — %s logbook, %s QSOs", call, count), nil
	case call != "":
		return fmt.Sprintf("Connected — %s logbook", call), nil
	default:
		return "Connected — key OK", nil
	}
}

// parseQRZResponse decodes QRZ's "&"-joined, URL-encoded reply.
func parseQRZResponse(body string) (UploadResult, error) {
	vals, err := url.ParseQuery(strings.TrimSpace(body))
	if err != nil {
		return UploadResult{}, fmt.Errorf("qrz: bad response %q: %w", body, err)
	}
	// The QRZ Logbook API returns the outcome in RESULT (=OK/FAIL/AUTH).
	// Accept STATUS as a fallback for robustness, but RESULT is the real
	// field — reading only STATUS made every INSERT (incl. successful ones)
	// look like it failed with an empty status.
	status := qrzStatusField(vals)
	reason := strings.TrimSpace(vals.Get("REASON"))
	logID := strings.TrimSpace(vals.Get("LOGID"))

	switch status {
	case "OK":
		return UploadResult{OK: true, LogID: logID, Message: reason}, nil
	case "FAIL":
		// A duplicate is a benign failure — the QSO is in the logbook, so
		// from our side the upload "succeeded". Detect it from the reason.
		if isDuplicateReason(reason) {
			return UploadResult{OK: true, LogID: logID, Message: "already in logbook"}, nil
		}
		return UploadResult{OK: false, Message: reason}, fmt.Errorf("qrz: upload failed: %s", reason)
	case "AUTH":
		return UploadResult{OK: false, Message: reason}, fmt.Errorf("qrz: auth error: %s", reason)
	default:
		return UploadResult{OK: false, Message: reason}, fmt.Errorf("qrz: unexpected status %q (%s)", status, reason)
	}
}

// qrzStatusField returns the QRZ outcome code, preferring RESULT (the
// Logbook API's real field) and falling back to STATUS.
func qrzStatusField(vals url.Values) string {
	if v := strings.ToUpper(strings.TrimSpace(vals.Get("RESULT"))); v != "" {
		return v
	}
	return strings.ToUpper(strings.TrimSpace(vals.Get("STATUS")))
}

// isDuplicateReason recognises the various phrasings QRZ uses when a QSO is
// already present.
func isDuplicateReason(reason string) bool {
	r := strings.ToLower(reason)
	return strings.Contains(r, "duplicate") ||
		strings.Contains(r, "already") ||
		strings.Contains(r, "unable to add") && strings.Contains(r, "exists")
}
