package extsvc

import (
	"archive/tar"
	"bytes"
	"compress/gzip"
	"context"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"net/url"
	"strings"
	"time"
)

// HamQTH real-time QSO upload.
//
// One POST per QSO to qso_realtime.php with the account username/password —
// the same credentials the HamQTH callbook lookup uses. The answer is the
// HTTP status code, not a body format: 200 saved, 400 rejected (bad band,
// duplicate…, reason in the body), 403 wrong credentials, 500 server error.
const hamqthUploadURL = "https://www.hamqth.com/qso_realtime.php"

// hamqthFullLogURL takes a WHOLE log as a file. Note "whole": HamQTH's own
// documentation says "you always have to upload whole log. HamQTH doesn't
// support partial upload" — the file REPLACES what is on the site. That is why
// it is not the batch path for a selection, and why the caller must have said
// so out loud before we get here.
const hamqthFullLogURL = "https://www.hamqth.com/prg_log_upload.php"

// hamqthMaxUpload is the documented ceiling for one upload.
const hamqthMaxUpload = 20 << 20

// hamqthCompressAbove is where a plain .adi stops being sent as text. Well
// under the limit: the multipart envelope and the form fields ride along too.
const hamqthCompressAbove = 12 << 20

// hamqthLoginURL is the callbook session login — the one authenticated HamQTH
// endpoint that cannot change anything in the log, which is what the settings
// Test button must call.
const hamqthLoginURL = "https://www.hamqth.com/xml.php"

// UploadHamQTH pushes one ADIF record to the HamQTH online logbook.
func UploadHamQTH(ctx context.Context, client *http.Client, cfg ServiceConfig, adifRecord string) (UploadResult, error) {
	return uploadHamQTHTo(ctx, client, hamqthUploadURL, cfg, adifRecord)
}

func uploadHamQTHTo(ctx context.Context, client *http.Client, endpoint string, cfg ServiceConfig, adifRecord string) (UploadResult, error) {
	user := strings.TrimSpace(cfg.Username)
	switch {
	case user == "":
		return UploadResult{}, fmt.Errorf("hamqth: username not set")
	case cfg.Password == "":
		return UploadResult{}, fmt.Errorf("hamqth: password not set")
	}
	rec := strings.TrimSpace(adifRecord)
	if rec == "" {
		return UploadResult{}, fmt.Errorf("hamqth: empty ADIF record")
	}
	form := url.Values{}
	form.Set("u", user)
	form.Set("p", cfg.Password)
	// c: the logbook callsign when the account holds several; empty means the
	// account's own call, which is the common case.
	if c := strings.ToUpper(strings.TrimSpace(cfg.Callsign)); c != "" {
		form.Set("c", c)
	}
	form.Set("adif", rec)
	form.Set("prg", "OpsLog")
	form.Set("cmd", "insert")

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(form.Encode()))
	if err != nil {
		return UploadResult{}, err
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	if client == nil {
		client = &http.Client{Timeout: 30 * time.Second}
	}
	resp, err := client.Do(req)
	if err != nil {
		return UploadResult{}, err
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
	msg := strings.TrimSpace(string(body))

	switch resp.StatusCode {
	case http.StatusOK:
		return UploadResult{OK: true}, nil
	case http.StatusBadRequest:
		// "Rejected" covers duplicates too. A duplicate is a SUCCESS for our
		// purposes — the QSO is in the logbook, retrying it forever isn't —
		// same treatment HRDLog's <insert>0 gets.
		if strings.Contains(strings.ToLower(msg), "dupl") {
			return UploadResult{OK: true, Ignored: true, Message: "already in logbook"}, nil
		}
		if msg == "" {
			msg = "QSO rejected"
		}
		return UploadResult{OK: false, Message: msg}, nil
	case http.StatusForbidden:
		return UploadResult{}, fmt.Errorf("hamqth: wrong username or password")
	default:
		if msg != "" && len(msg) < 200 {
			return UploadResult{}, fmt.Errorf("hamqth: HTTP %d: %s", resp.StatusCode, msg)
		}
		return UploadResult{}, fmt.Errorf("hamqth: HTTP %d", resp.StatusCode)
	}
}

// UploadHamQTHFullLog replaces the account's log with the given ADIF.
//
// DESTRUCTIVE by design of the remote API, not by ours: everything on HamQTH
// for this callsign that is not in this file stops existing. The caller owns
// the confirmation.
//
// The file goes in the multipart field "f" (HamQTH's own curl example:
// curl -F f=@log.adi -F send_log=OK -F u=… -F p=…). A large log is sent as a
// tar.gz — one of the archive formats the site unpacks — because the ceiling is
// 20 MB and a six-figure log passes it as plain text.
func UploadHamQTHFullLog(ctx context.Context, client *http.Client, cfg ServiceConfig, adifText string) (UploadResult, error) {
	user := strings.TrimSpace(cfg.Username)
	switch {
	case user == "":
		return UploadResult{}, fmt.Errorf("hamqth: username not set")
	case cfg.Password == "":
		return UploadResult{}, fmt.Errorf("hamqth: password not set")
	case strings.TrimSpace(adifText) == "":
		return UploadResult{}, fmt.Errorf("hamqth: nothing to upload")
	}

	payload := []byte(adifText)
	name := "opslog.adi"
	if len(payload) > hamqthCompressAbove {
		gz, err := tarGzADIF(payload)
		if err != nil {
			return UploadResult{}, fmt.Errorf("hamqth: compressing the log: %w", err)
		}
		payload, name = gz, "opslog.tar.gz"
	}
	if len(payload) > hamqthMaxUpload {
		return UploadResult{}, fmt.Errorf("hamqth: the log is %d MB compressed, over HamQTH's %d MB limit",
			len(payload)>>20, hamqthMaxUpload>>20)
	}

	var body bytes.Buffer
	mw := multipart.NewWriter(&body)
	_ = mw.WriteField("u", user)
	_ = mw.WriteField("p", cfg.Password)
	if c := strings.ToUpper(strings.TrimSpace(cfg.Callsign)); c != "" {
		_ = mw.WriteField("c", c)
	}
	_ = mw.WriteField("send_log", "OK")
	fw, err := mw.CreateFormFile("f", name)
	if err != nil {
		return UploadResult{}, err
	}
	if _, err := fw.Write(payload); err != nil {
		return UploadResult{}, err
	}
	if err := mw.Close(); err != nil {
		return UploadResult{}, err
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, hamqthFullLogURL, &body)
	if err != nil {
		return UploadResult{}, err
	}
	req.Header.Set("Content-Type", mw.FormDataContentType())
	if client == nil {
		// A whole log is a long POST on a slow uplink.
		client = &http.Client{Timeout: 10 * time.Minute}
	}
	resp, err := client.Do(req)
	if err != nil {
		return UploadResult{}, err
	}
	defer resp.Body.Close()
	raw, _ := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
	msg := strings.TrimSpace(string(raw))
	if looksLikeHTML(msg) {
		msg = ""
	}
	if resp.StatusCode != http.StatusOK {
		if msg != "" && len(msg) < 300 {
			return UploadResult{}, fmt.Errorf("hamqth: HTTP %d: %s", resp.StatusCode, msg)
		}
		return UploadResult{}, fmt.Errorf("hamqth: HTTP %d", resp.StatusCode)
	}
	// The site answers in prose, and only its own refusals are worth reading
	// back: the ADIF itself is validated later, in the background, and any
	// complaint about it reaches the operator by e-mail rather than here.
	low := strings.ToLower(msg)
	switch {
	case strings.Contains(low, "successfully"):
		return UploadResult{OK: true, Message: msg}, nil
	case strings.Contains(low, "wrong username"), strings.Contains(low, "password"):
		return UploadResult{}, fmt.Errorf("hamqth: %s", msg)
	case strings.Contains(low, "cannot upload log for this callsign"):
		return UploadResult{}, fmt.Errorf("hamqth: %s", msg)
	case msg == "":
		// HTTP 200 with nothing to say: taken as accepted, and said so.
		return UploadResult{OK: true, Message: "uploaded (no reply text)"}, nil
	default:
		return UploadResult{OK: false, Message: msg}, nil
	}
}

// tarGzADIF wraps the ADIF as log.adi inside a tar.gz — the archive must carry
// a .adi/.adif member for HamQTH to find the log in it.
func tarGzADIF(adif []byte) ([]byte, error) {
	var out bytes.Buffer
	gz := gzip.NewWriter(&out)
	tw := tar.NewWriter(gz)
	if err := tw.WriteHeader(&tar.Header{
		Name: "opslog.adi", Mode: 0o644, Size: int64(len(adif)),
	}); err != nil {
		return nil, err
	}
	if _, err := tw.Write(adif); err != nil {
		return nil, err
	}
	if err := tw.Close(); err != nil {
		return nil, err
	}
	if err := gz.Close(); err != nil {
		return nil, err
	}
	return out.Bytes(), nil
}

// TestHamQTH verifies the credentials against the callbook session login —
// authenticated, and unable to touch the log.
func TestHamQTH(ctx context.Context, client *http.Client, cfg ServiceConfig) (string, error) {
	user := strings.TrimSpace(cfg.Username)
	if user == "" || cfg.Password == "" {
		return "", fmt.Errorf("hamqth: set the username and password first")
	}
	q := url.Values{}
	q.Set("u", user)
	q.Set("p", cfg.Password)
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, hamqthLoginURL+"?"+q.Encode(), nil)
	if err != nil {
		return "", err
	}
	if client == nil {
		client = &http.Client{Timeout: 30 * time.Second}
	}
	resp, err := client.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
	s := string(body)
	if strings.Contains(s, "<session_id>") {
		return fmt.Sprintf("Connected to HamQTH as %s.", user), nil
	}
	if i := strings.Index(s, "<error>"); i >= 0 {
		e := s[i+len("<error>"):]
		if j := strings.Index(e, "</error>"); j >= 0 {
			return "", fmt.Errorf("hamqth: %s", strings.TrimSpace(e[:j]))
		}
	}
	return "", fmt.Errorf("hamqth: unexpected answer — check the username and password")
}
