package lookup

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

// QRZ is a lookup.Provider for xmldata.qrz.com.
// A QRZ subscription is required for full data; basic info is free.
type QRZ struct {
	User     string
	Password string

	HTTP *http.Client

	// PreferNickname takes QRZ's <nickname> over the composed first+last name
	// when the operator has published one. It is the name they go BY on the air,
	// which is what belongs in a log — HamQTH's <nick> is already used that way,
	// and this brings QRZ into line for operators who want it.
	PreferNickname bool

	mu       sync.Mutex
	session  string
	loggedAt time.Time
}

func NewQRZ(user, password string) *QRZ {
	return &QRZ{
		User:     user,
		Password: password,
		HTTP:     &http.Client{Timeout: 12 * time.Second},
	}
}

func (q *QRZ) Name() string { return "qrz" }

// Lookup queries QRZ for the given callsign.
func (q *QRZ) Lookup(ctx context.Context, callsign string) (Result, error) {
	if q.User == "" || q.Password == "" {
		return Result{}, fmt.Errorf("qrz: credentials not set")
	}
	key, err := q.sessionKey(ctx)
	if err != nil {
		return Result{}, err
	}
	r, err := q.fetch(ctx, key, callsign)
	if err == errQRZSessionExpired {
		// Force re-login and retry once.
		q.mu.Lock()
		q.session = ""
		q.mu.Unlock()
		key, err = q.sessionKey(ctx)
		if err != nil {
			return Result{}, err
		}
		r, err = q.fetch(ctx, key, callsign)
	}
	return r, err
}

func (q *QRZ) sessionKey(ctx context.Context) (string, error) {
	q.mu.Lock()
	defer q.mu.Unlock()
	// QRZ sessions are valid ~24h; re-login defensively after 12h.
	if q.session != "" && time.Since(q.loggedAt) < 12*time.Hour {
		return q.session, nil
	}
	u := fmt.Sprintf("https://xmldata.qrz.com/xml/current/?username=%s;password=%s;agent=HamLog",
		url.QueryEscape(q.User), url.QueryEscape(q.Password))
	body, err := q.get(ctx, u)
	if err != nil {
		return "", err
	}
	var resp qrzDB
	if err := xml.Unmarshal(body, &resp); err != nil {
		return "", fmt.Errorf("qrz: parse session: %w", err)
	}
	if resp.Session.Error != "" {
		return "", fmt.Errorf("qrz login: %s", resp.Session.Error)
	}
	if resp.Session.Key == "" {
		return "", fmt.Errorf("qrz: empty session key")
	}
	q.session = resp.Session.Key
	q.loggedAt = time.Now()
	return q.session, nil
}

var errQRZSessionExpired = fmt.Errorf("qrz: session expired")

func (q *QRZ) fetch(ctx context.Context, sessionKey, callsign string) (Result, error) {
	u := fmt.Sprintf("https://xmldata.qrz.com/xml/current/?s=%s;callsign=%s",
		url.QueryEscape(sessionKey), url.QueryEscape(callsign))
	body, err := q.get(ctx, u)
	if err != nil {
		return Result{}, err
	}
	var resp qrzDB
	if err := xml.Unmarshal(body, &resp); err != nil {
		return Result{}, fmt.Errorf("qrz: parse callsign: %w", err)
	}
	if resp.Session.Error != "" {
		msg := strings.ToLower(resp.Session.Error)
		if strings.Contains(msg, "session") || strings.Contains(msg, "invalid") {
			return Result{}, errQRZSessionExpired
		}
		if strings.Contains(msg, "not found") {
			return Result{}, ErrNotFound
		}
		return Result{}, fmt.Errorf("qrz: %s", resp.Session.Error)
	}
	c := resp.Callsign
	if c.Call == "" {
		return Result{}, ErrNotFound
	}
	r := Result{
		Callsign:  strings.ToUpper(c.Call),
		Name:      qrzName(q.PreferNickname, c.Nickname, c.FName, c.Name),
		QTH:       c.Addr2,
		Address:   composeQRZAddress(c.Addr1, c.Addr2, c.Zip, c.Country),
		State:     strings.ToUpper(c.State),
		County:    c.County,
		Country:   c.Country,
		Grid:      strings.ToUpper(c.Grid),
		Continent: strings.ToUpper(c.Continent),
		Email:     c.Email,
		QSLVia:    c.QSLMgr,
		ImageURL:  strings.TrimSpace(c.Image),
		IOTA:      strings.ToUpper(strings.TrimSpace(c.IOTA)),
	}
	r.Lat, _ = strconv.ParseFloat(c.Lat, 64)
	r.Lon, _ = strconv.ParseFloat(c.Lon, 64)
	r.DXCC, _ = strconv.Atoi(c.DXCC)
	r.CQZ, _ = strconv.Atoi(c.CQZone)
	r.ITUZ, _ = strconv.Atoi(c.ITUZone)
	return r, nil
}

func (q *QRZ) get(ctx context.Context, u string) ([]byte, error) {
	req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
	if err != nil {
		return nil, err
	}
	resp, err := q.HTTP.Do(req)
	if err != nil {
		return nil, fmt.Errorf("qrz http: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		return nil, fmt.Errorf("qrz http %d", resp.StatusCode)
	}
	return io.ReadAll(resp.Body)
}

// ----- XML shapes -----

type qrzDB struct {
	XMLName  xml.Name    `xml:"QRZDatabase"`
	Session  qrzSession  `xml:"Session"`
	Callsign qrzCallsign `xml:"Callsign"`
}

type qrzSession struct {
	Key   string `xml:"Key"`
	Error string `xml:"Error"`
}

type qrzCallsign struct {
	Call      string `xml:"call"`
	FName     string `xml:"fname"`
	Nickname  string `xml:"nickname"` // the name the operator goes by on the air
	Name      string `xml:"name"`
	Addr1     string `xml:"addr1"`
	Addr2     string `xml:"addr2"`
	Zip       string `xml:"zip"`
	State     string `xml:"state"`
	County    string `xml:"county"`
	Country   string `xml:"country"`
	Grid      string `xml:"grid"`
	Lat       string `xml:"lat"`
	Lon       string `xml:"lon"`
	DXCC      string `xml:"dxcc"`
	CQZone    string `xml:"cqzone"`
	ITUZone   string `xml:"ituzone"`
	Continent string `xml:"cont"`
	Email     string `xml:"email"`
	QSLMgr    string `xml:"qslmgr"`
	Image     string `xml:"image"` // direct URL to the profile picture (subscribers only on QRZ)
	IOTA      string `xml:"iota"`  // island reference for an operator on one, e.g. EU-048
}

// composeQRZAddress builds a multi-line postal address from QRZ's separate
// fields (street, city, zip, country) the same way Log4OM displays it.
// addr1 is the street, addr2 is the city — skip addr1 when it duplicates
// the city (common for users who only filled the city).
func composeQRZAddress(addr1, addr2, zip, country string) string {
	addr1 = strings.TrimSpace(addr1)
	addr2 = strings.TrimSpace(addr2)
	zip = strings.TrimSpace(zip)
	country = strings.TrimSpace(country)
	var parts []string
	if addr1 != "" && !strings.EqualFold(addr1, addr2) {
		parts = append(parts, addr1)
	}
	if addr2 != "" {
		parts = append(parts, addr2)
	}
	if zip != "" {
		parts = append(parts, zip)
	}
	if country != "" {
		parts = append(parts, country)
	}
	return strings.Join(parts, ", ")
}

func firstNonEmpty(s ...string) string {
	for _, v := range s {
		v = strings.TrimSpace(v)
		if v != "" {
			return v
		}
	}
	return ""
}

// qrzName picks what goes in the log's Name field.
//
// The FIRST name only. A log greets an operator, it does not address an
// envelope: "Robert" is what goes out on the air, and "Robert Smith" filled the
// field with something no one would ever send. The surname stays available in
// the record; it simply is not the name of the contact.
//
// The nickname is taken ahead of it when the operator asked for that AND QRZ
// has one — a blank nickname must never blank the name, which is the whole
// reason this is a fallback rather than a swap.
//
// The surname is the last resort: a record with no first name at all is better
// answered with a surname than with nothing.
func qrzName(preferNickname bool, nickname, fname, name string) string {
	if preferNickname {
		if n := strings.TrimSpace(nickname); n != "" {
			return n
		}
	}
	if f := strings.TrimSpace(fname); f != "" {
		return f
	}
	return strings.TrimSpace(name)
}
