package extsvc

// Withdrawing a QSO from the external services after it is deleted locally.
//
// The two services could hardly be less alike, and the difference decides what
// is possible for QSOs already in the log:
//
//   - QRZ.com deletes ONLY by logid — the identifier it returns on INSERT.
//     There is no delete-by-callsign-and-date. A QSO uploaded before OpsLog
//     started recording that identifier therefore cannot be withdrawn from
//     here; the operator has to do it on the website. QRZ's own documentation
//     puts it plainly: "There is no undo from this operation."
//   - Club Log deletes by MATCH: the worked callsign, the exact timestamp and
//     the band. Nothing has to have been stored in advance, so this works for
//     every QSO in the log, past or future.

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

// clublogDeleteURL is Club Log's real-time single-QSO delete endpoint.
const clublogDeleteURL = "https://clublog.org/delete.php"

// DeleteQRZ removes QSOs from a QRZ logbook by their logid.
//
// The identifiers come from the LOGID QRZ returns on INSERT, which OpsLog
// stores on the QSO as APP_OPSLOG_QRZ_LOGID.
//
// QRZ answers RESULT=OK when every id was deleted, PARTIAL when some were not
// found, FAIL when none were. PARTIAL and FAIL are NOT treated as errors here:
// the QSO is being deleted locally either way, and "it was already gone from
// QRZ" is the outcome the operator wanted. The message is returned so the
// caller can log what actually happened.
func DeleteQRZ(ctx context.Context, client *http.Client, apiKey string, logIDs []string) (string, error) {
	apiKey = strings.TrimSpace(apiKey)
	if apiKey == "" {
		return "", fmt.Errorf("qrz: api key not set")
	}
	ids := make([]string, 0, len(logIDs))
	for _, id := range logIDs {
		if id = strings.TrimSpace(id); id != "" {
			ids = append(ids, id)
		}
	}
	if len(ids) == 0 {
		return "", fmt.Errorf("qrz: no logid recorded for this QSO — it can only be removed on qrz.com")
	}

	form := url.Values{}
	form.Set("KEY", apiKey)
	form.Set("ACTION", "DELETE")
	form.Set("LOGIDS", strings.Join(ids, ","))

	body, err := postForm(ctx, client, qrzAPIURL, form, 20*time.Second)
	if err != nil {
		return "", fmt.Errorf("qrz: %w", err)
	}
	LogSink("qrz: DELETE raw response: %s", strings.TrimSpace(body))

	vals, _ := url.ParseQuery(strings.TrimSpace(body))
	result := strings.ToUpper(strings.TrimSpace(vals.Get("RESULT")))
	count := strings.TrimSpace(vals.Get("COUNT"))
	switch result {
	case "OK", "PARTIAL", "FAIL":
		return fmt.Sprintf("RESULT=%s COUNT=%s", result, count), nil
	case "AUTH":
		return "", fmt.Errorf("qrz: the logbook key was refused")
	}
	return "", fmt.Errorf("qrz: unexpected reply %q", strings.TrimSpace(body))
}

// DeleteClublog removes one QSO from a Club Log logbook.
//
// whenUTC must be the QSO's start time; Club Log matches on it EXACTLY, to the
// second, so a rounded or local-time value simply finds nothing. band is an
// ADIF band name ("20m"), translated below to the band number Club Log wants.
func DeleteClublog(ctx context.Context, client *http.Client, cfg ServiceConfig, dxCall string, whenUTC time.Time, band string) (string, error) {
	email := strings.TrimSpace(cfg.Email)
	call := strings.ToUpper(strings.TrimSpace(cfg.Callsign))
	dx := strings.ToUpper(strings.TrimSpace(dxCall))
	switch {
	case email == "":
		return "", fmt.Errorf("clublog: account email not set")
	case cfg.Password == "":
		return "", fmt.Errorf("clublog: password not set")
	case call == "":
		return "", fmt.Errorf("clublog: logbook callsign not set")
	case dx == "":
		return "", fmt.Errorf("clublog: no callsign to delete")
	case whenUTC.IsZero():
		return "", fmt.Errorf("clublog: the QSO has no time — Club Log matches on it exactly")
	}
	bandID, ok := clublogBandID(band)
	if !ok {
		return "", fmt.Errorf("clublog: band %q is not one Club Log accepts for deletion", band)
	}

	form := url.Values{}
	form.Set("email", email)
	form.Set("password", cfg.Password)
	form.Set("callsign", call)
	form.Set("dxcall", dx)
	form.Set("datetime", whenUTC.UTC().Format("2006-01-02 15:04:05"))
	form.Set("bandid", strconv.Itoa(bandID))
	api := strings.TrimSpace(cfg.APIKey)
	if api == "" {
		api = clublogAppAPIKey
	}
	form.Set("api", api)

	body, err := postForm(ctx, client, clublogDeleteURL, form, 20*time.Second)
	if err != nil {
		return "", fmt.Errorf("clublog: %w", err)
	}
	msg := strings.TrimSpace(body)
	LogSink("clublog: DELETE raw response: %s", msg)
	// "QSO Not Deleted" means no record matched. That is not an error worth
	// stopping for — the QSO is going away locally regardless, and a QSO absent
	// from Club Log is the state being asked for.
	return msg, nil
}

// clublogBandID maps an ADIF band name to Club Log's band number.
//
// The list is Club Log's own, and it is deliberately NOT derived from the
// frequency: Club Log accepts these values and no others, so a band outside it
// must be reported rather than approximated to a neighbour.
func clublogBandID(band string) (int, bool) {
	switch strings.ToLower(strings.TrimSpace(band)) {
	case "160m":
		return 160, true
	case "80m":
		return 80, true
	case "60m":
		return 60, true
	case "40m":
		return 40, true
	case "30m":
		return 30, true
	case "20m":
		return 20, true
	case "17m":
		return 17, true
	case "15m":
		return 15, true
	case "12m":
		return 12, true
	case "10m":
		return 10, true
	case "6m":
		return 6, true
	case "4m":
		return 4, true
	case "2m":
		return 2, true
	case "70cm":
		return 70, true
	case "23cm":
		return 23, true
	case "13cm":
		return 13, true
	}
	return 0, false
}

// postForm is the shared request/read for both deletes.
func postForm(ctx context.Context, client *http.Client, endpoint string, form url.Values, timeout time.Duration) (string, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(form.Encode()))
	if err != nil {
		return "", fmt.Errorf("build request: %w", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("User-Agent", clublogUserAgent)
	if client == nil {
		client = &http.Client{Timeout: timeout}
	}
	resp, err := client.Do(req)
	if err != nil {
		return "", fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()
	raw, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
	if err != nil {
		return "", fmt.Errorf("read response: %w", err)
	}
	b := string(raw)
	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("http %d: %s", resp.StatusCode, strings.TrimSpace(b))
	}
	return b, nil
}
