package extsvc

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"
)

// HAMLOG.online — a cloud logbook whose confirmations feed its own award
// programme, so operators want their contacts there as they make them.
//
// # Where this protocol comes from
//
// HAMLOG publishes no API documentation. What follows is read from THEIR OWN
// client, the HAMLOG Agent (github.com/hamlogonline/Agent, Hamlog/hamlog_api.py)
// — the authoritative source short of asking them, and the same code their own
// users run:
//
//	POST https://hamlog.online/api/agent/     (JSON in, JSON out)
//
//	{"KEYSTATUS": {"APIKEY": k}}                  → {"STATUS":"OK","CALLSIGN":…,"EXPIRES":…}
//	{"ADIFADD":   {"APIKEY": k, "ADIFDATA": rec}} → {"STATUS":"OK"}
//	{"QSOADD":    {"APIKEY": k, "DATA": {…}}}     → field map, keys upper-cased
//	{"LOGOUT":    {"APIKEY": k}}
//
// A failure answers {"ERROR": "…"} with no STATUS, so success is "STATUS is
// exactly OK" rather than "no error field" — an unknown reply shape must not
// read as an accepted QSO.
//
// ADIFADD is the verb used here: OpsLog already builds a full ADIF record for
// every other service, and sending the same bytes keeps one representation of
// a contact instead of two.
//
// The operator gets their key from https://hamlog.online/account/agent.php.
const (
	hamlogAPIEndpoint = "https://hamlog.online/api/agent/"
	hamlogKeyPage     = "https://hamlog.online/account/agent.php"
)

// hamlogReply is the shape both success and failure share.
type hamlogReply struct {
	Status   string `json:"STATUS"`
	Error    string `json:"ERROR"`
	Callsign string `json:"CALLSIGN"`
	Expires  any    `json:"EXPIRES"` // seconds since the epoch; string or number depending on the verb
}

// hamlogPost sends one verb and decodes the reply.
func hamlogPost(ctx context.Context, client *http.Client, endpoint string, body map[string]any) (hamlogReply, error) {
	buf, err := json.Marshal(body)
	if err != nil {
		return hamlogReply{}, fmt.Errorf("hamlog: encode request: %w", err)
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(buf))
	if err != nil {
		return hamlogReply{}, fmt.Errorf("hamlog: build request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json")
	if client == nil {
		client = &http.Client{Timeout: 20 * time.Second}
	}
	resp, err := client.Do(req)
	if err != nil {
		return hamlogReply{}, fmt.Errorf("hamlog: request failed: %w", err)
	}
	defer resp.Body.Close()
	raw, _ := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
	var r hamlogReply
	if jerr := json.Unmarshal(raw, &r); jerr != nil {
		// Not JSON at all — a proxy error page, a maintenance notice. Report what
		// arrived rather than "invalid character '<'", which tells an operator
		// nothing about their own setup.
		msg := strings.TrimSpace(string(raw))
		if len(msg) > 200 {
			msg = msg[:200]
		}
		if msg == "" {
			msg = fmt.Sprintf("HTTP %d", resp.StatusCode)
		}
		return hamlogReply{}, fmt.Errorf("hamlog: unexpected reply: %s", msg)
	}
	return r, nil
}

// hamlogFailure turns a reply into a human-readable reason, or "" on success.
func hamlogFailure(r hamlogReply) string {
	if strings.EqualFold(strings.TrimSpace(r.Status), "OK") {
		return ""
	}
	if e := strings.TrimSpace(r.Error); e != "" {
		return e
	}
	return "rejected"
}

// UploadHamlog pushes one ADIF record to HAMLOG.online.
func UploadHamlog(ctx context.Context, client *http.Client, cfg ServiceConfig, adifRecord string) (UploadResult, error) {
	return uploadHamlogTo(ctx, client, hamlogAPIEndpoint, cfg, adifRecord)
}

// ErrHamlogClosed is why nothing is sent to HAMLOG.online any more.
//
// The site stopped issuing API keys, and the upload API takes nothing else. An
// operator without a key cannot obtain one, and one WITH an old key is the
// exception this cannot be built around — so the door is closed here rather
// than left ajar for a request that can only fail.
//
// The code stays: their confirmations still arrive as an ADIF FILE (QSL Manager
// → HAMLOG.online → Import confirmations), which never needed a key, and the
// sent/received state already in operators' logs stays readable, filterable and
// bulk-editable.
var ErrHamlogClosed = errors.New("hamlog: HAMLOG.online no longer issues API keys, so uploading is not possible — their confirmations can still be imported from a file")

func uploadHamlogTo(ctx context.Context, client *http.Client, endpoint string, cfg ServiceConfig, adifRecord string) (UploadResult, error) {
	return UploadResult{}, ErrHamlogClosed
}

// uploadHamlogLive is the upload as it was, kept whole against the day keys
// come back. Nothing calls it.
func uploadHamlogLive(ctx context.Context, client *http.Client, endpoint string, cfg ServiceConfig, adifRecord string) (UploadResult, error) {
	key := strings.TrimSpace(cfg.APIKey)
	if key == "" {
		return UploadResult{}, fmt.Errorf("hamlog: API key not set — get one at %s", hamlogKeyPage)
	}
	rec := strings.TrimSpace(adifRecord)
	if rec == "" {
		return UploadResult{}, fmt.Errorf("hamlog: empty ADIF record")
	}
	r, err := hamlogPost(ctx, client, endpoint, map[string]any{
		"ADIFADD": map[string]any{"APIKEY": key, "ADIFDATA": rec},
	})
	if err != nil {
		return UploadResult{}, err
	}
	if reason := hamlogFailure(r); reason != "" {
		return UploadResult{OK: false, Message: reason}, nil
	}
	return UploadResult{OK: true}, nil
}

// CheckHamlogKey validates an API key and reports the callsign it belongs to.
//
// Worth its own call because HAMLOG offers what no other service here does: the
// key can be checked BEFORE the first QSO, and the answer names the account. An
// operator who pasted the key of another callsign — or one that has expired —
// finds out in the settings panel rather than through a week of silent refusals.
func CheckHamlogKey(ctx context.Context, client *http.Client, key string) (callsign string, err error) {
	key = strings.TrimSpace(key)
	if key == "" {
		return "", fmt.Errorf("hamlog: API key not set — get one at %s", hamlogKeyPage)
	}
	r, perr := hamlogPost(ctx, client, hamlogAPIEndpoint, map[string]any{
		"KEYSTATUS": map[string]any{"APIKEY": key},
	})
	if perr != nil {
		return "", perr
	}
	if reason := hamlogFailure(r); reason != "" {
		return "", fmt.Errorf("hamlog: %s", reason)
	}
	return strings.ToUpper(strings.TrimSpace(r.Callsign)), nil
}
