#!/usr/bin/env python3
"""
Bulk ARRL NTS MPG-1 Radiogram Generator with Tkinter GUI.
"""

from __future__ import annotations

import argparse
import configparser
import re
import string
from pathlib import Path
from typing import Callable

import requests
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import xml.etree.ElementTree as ET

QRZ_URL = "https://xmldata.qrz.com/xml/current/"
CONFIG_FILE = Path("config.ini")

DEFAULTS = {
    "message_number": "XXX",
    "precedence": "R",
    "station_of_origin": "XX8XXX",
    "place_of_origin": "STOW OH",
    "date_filed": "XXXNN",
    "signature": "ANTHONY LUSCRE K8ZT W1AW/8 OHIO COORDINATOR",
    "raw_message_text": (
        "THANK YOU FOR ON HYPHEN AIR QSO WITH W1AW/8 OHIO DURING THE ARRL "
        "SPONSORED AMERICA 250 OPERATION X ALL LOGS WILL BE UPLOADED TO ARRL LOTW"
    ),
    "callsign_file": "callsigns.txt",
    "output_dir": "radiograms",
}


def qrz_login(username: str, password: str) -> str:
    params = {"username": username, "password": password}
    response = requests.get(QRZ_URL, params=params, timeout=20)
    response.raise_for_status()

    root = ET.fromstring(response.text)
    ns = {"q": "http://xmldata.qrz.com"}
    key = root.find(".//q:Session/q:Key", ns)
    if key is None or not (key.text or "").strip():
        error = root.find(".//q:Session/q:Error", ns)
        reason = (error.text or "").strip() if error is not None else "missing session key"
        raise RuntimeError(f"QRZ login failed: {reason}")
    return key.text.strip()


def qrz_lookup(session_key: str, callsign: str) -> dict[str, str]:
    params = {"s": session_key, "callsign": callsign}
    response = requests.get(QRZ_URL, params=params, timeout=20)
    response.raise_for_status()

    root = ET.fromstring(response.text)
    ns = {"q": "http://xmldata.qrz.com"}
    error = root.find(".//q:Session/q:Error", ns)
    if error is not None and (error.text or "").strip():
        raise RuntimeError(f"QRZ lookup failed for {callsign}: {(error.text or '').strip()}")

    data: dict[str, str] = {}
    for elem in root.findall(".//q:Callsign/*", ns):
        tag = elem.tag.split("}", 1)[1]
        data[tag.lower()] = elem.text or ""
    if not data:
        raise RuntimeError(f"No callsign data returned for {callsign}.")
    return data


def nts_clean_address(text: str) -> str:
    cleaned = text.translate(str.maketrans("", "", string.punctuation))
    return " ".join(cleaned.upper().split())


def nts_format_email(email: str) -> str:
    formatted = email.strip().upper().replace(".", " DOT ").replace("@", " ATSIGN ")
    return " ".join(formatted.split())


def nts_zip5(zipcode: str) -> str:
    digits = "".join(ch for ch in zipcode.strip() if ch.isdigit())
    return digits[:5] if len(digits) >= 5 else digits


def nts_city(info: dict[str, str]) -> str:
    city = info.get("city", "").strip()
    if city:
        return nts_clean_address(city)
    addr2 = info.get("addr2", "")
    cleaned = nts_clean_address(addr2)
    return cleaned.split()[0] if cleaned else ""


def nts_state(info: dict[str, str]) -> str:
    state = info.get("state", "").strip()
    if state:
        return nts_clean_address(state)
    addr2 = info.get("addr2", "")
    cleaned = nts_clean_address(addr2)
    parts = cleaned.split()
    return parts[1] if len(parts) >= 2 else ""


def nts_addressee(info: dict[str, str]) -> str:
    parts = [
        nts_clean_address(info.get("fname", "")),
        nts_clean_address(info.get("name", "")),
        nts_clean_address(info.get("lname", "")),
        info.get("call", "").upper(),
    ]
    return " ".join(p for p in parts if p)


def nts_format_text(raw: str) -> str:
    words = re.sub(r"\s+", " ", raw.upper()).strip().split()
    return "\n".join(" ".join(words[i:i + 5]) for i in range(0, len(words), 5))


def nts_check(raw: str) -> int:
    return len(re.sub(r"\s+", " ", raw.upper()).strip().split())


def build_preamble(values: dict[str, str], check: int) -> str:
    return (
        f"NR {values['message_number']} {values['precedence']} HXG {values['station_of_origin']} "
        f"CHECK {check} {values['place_of_origin']} {values['date_filed']}"
    )


def build_radiogram(values: dict[str, str], info: dict[str, str], email_formatted: str) -> str:
    to_line = nts_addressee(info)
    addr1 = nts_clean_address(info.get("addr1", ""))
    city = nts_city(info)
    state = nts_state(info)
    zipc = nts_zip5(info.get("zip", ""))
    check = nts_check(values["raw_message_text"])
    preamble = build_preamble(values, check)
    text_block = nts_format_text(values["raw_message_text"])
    return f"""{preamble}
{to_line}
{addr1}
{city} {state} {zipc}
EMAIL {email_formatted}

BT
{text_block}
BT

{values['signature']}
AR
""".strip()


def load_app_config(config_path: Path) -> dict[str, str]:
    data = DEFAULTS.copy()
    parser = configparser.ConfigParser()
    if not config_path.exists():
        return data

    parser.read(config_path)
    if parser.has_section("QRZ"):
        data["username"] = parser.get("QRZ", "username", fallback="")
        data["password"] = parser.get("QRZ", "password", fallback="")
    else:
        data["username"] = ""
        data["password"] = ""

    if parser.has_section("MESSAGE"):
        for key in DEFAULTS:
            data[key] = parser.get("MESSAGE", key, fallback=DEFAULTS[key])
    return data


def save_app_config(config_path: Path, values: dict[str, str]) -> None:
    parser = configparser.ConfigParser()
    parser["QRZ"] = {
        "username": values.get("username", ""),
        "password": values.get("password", ""),
    }
    parser["MESSAGE"] = {key: values[key] for key in DEFAULTS}
    with config_path.open("w", encoding="utf-8") as fp:
        parser.write(fp)


def read_callsigns_from_file(path: Path) -> list[str]:
    return [line.strip().upper() for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]


def read_callsigns_from_text(text: str) -> list[str]:
    callsigns = [line.strip().upper() for line in text.splitlines() if line.strip()]
    deduped: list[str] = []
    seen: set[str] = set()
    for callsign in callsigns:
        if callsign not in seen:
            seen.add(callsign)
            deduped.append(callsign)
    return deduped


def generate_bulk(values: dict[str, str], callsigns: list[str], log: Callable[[str], None]) -> tuple[int, int]:
    if not callsigns:
        raise RuntimeError("No callsigns provided.")

    output_dir = Path(values["output_dir"])
    output_dir.mkdir(parents=True, exist_ok=True)

    log("Logging into QRZ XML API...")
    session_key = qrz_login(values["username"], values["password"])
    log("QRZ login successful.")

    generated = 0
    skipped = 0
    for cs in callsigns:
        log(f"Looking up {cs}...")
        try:
            info = qrz_lookup(session_key, cs)
        except (RuntimeError, requests.RequestException, ET.ParseError) as exc:
            skipped += 1
            log(f"  Lookup failed: {exc}")
            continue

        email = info.get("email", "").strip()
        if not email:
            skipped += 1
            log("  Skipped: no email in QRZ profile.")
            continue

        radiogram = build_radiogram(values, info, nts_format_email(email))
        outfile = output_dir / f"{cs}.txt"
        outfile.write_text(radiogram, encoding="utf-8")
        generated += 1
        log(f"  Generated -> {outfile}")

    return generated, skipped


class RadiogramGui(tk.Tk):
    def __init__(self) -> None:
        super().__init__()
        self.title("Radiogram Bulk Generator")
        self.geometry("980x780")

        self.vars: dict[str, tk.StringVar] = {}
        self._build_form()
        self._load_defaults()

    def _build_form(self) -> None:
        frame = ttk.Frame(self, padding=12)
        frame.pack(fill=tk.BOTH, expand=True)

        fields = [
            ("QRZ Username", "username"),
            ("QRZ Password", "password"),
            ("Message Number", "message_number"),
            ("Precedence", "precedence"),
            ("Station of Origin", "station_of_origin"),
            ("Place of Origin", "place_of_origin"),
            ("Date Filed", "date_filed"),
            ("Signature", "signature"),
            ("Callsign File", "callsign_file"),
            ("Output Directory", "output_dir"),
        ]

        for row, (label, key) in enumerate(fields):
            ttk.Label(frame, text=label).grid(row=row, column=0, sticky="w", padx=(0, 8), pady=4)
            var = tk.StringVar()
            self.vars[key] = var
            show = "*" if key == "password" else ""
            entry = ttk.Entry(frame, textvariable=var, show=show, width=90)
            entry.grid(row=row, column=1, sticky="ew", pady=4)

            if key == "callsign_file":
                ttk.Button(frame, text="Browse", command=self._pick_callsign_file).grid(row=row, column=2, padx=(8, 0))
            elif key == "output_dir":
                ttk.Button(frame, text="Browse", command=self._pick_output_dir).grid(row=row, column=2, padx=(8, 0))

        message_row = len(fields)
        ttk.Label(frame, text="Message Text").grid(row=message_row, column=0, sticky="nw", padx=(0, 8), pady=4)
        self.message_text = tk.Text(frame, height=6, width=70)
        self.message_text.grid(row=message_row, column=1, sticky="ew", pady=4)

        callsign_row = message_row + 1
        ttk.Label(frame, text="Inline Callsigns (optional)").grid(row=callsign_row, column=0, sticky="nw", padx=(0, 8), pady=4)
        self.inline_callsigns = tk.Text(frame, height=6, width=70)
        self.inline_callsigns.grid(row=callsign_row, column=1, sticky="ew", pady=4)

        button_row = callsign_row + 1
        btns = ttk.Frame(frame)
        btns.grid(row=button_row, column=1, sticky="w", pady=10)
        ttk.Button(btns, text="Load Config", command=self._load_defaults).pack(side=tk.LEFT, padx=(0, 6))
        ttk.Button(btns, text="Save Config", command=self._save_defaults).pack(side=tk.LEFT, padx=(0, 6))
        ttk.Button(btns, text="Generate from File", command=self._generate_from_file).pack(side=tk.LEFT, padx=(0, 6))
        ttk.Button(btns, text="Generate from Inline", command=self._generate_from_inline).pack(side=tk.LEFT)

        log_row = button_row + 1
        ttk.Label(frame, text="Log").grid(row=log_row, column=0, sticky="nw", padx=(0, 8), pady=4)
        self.log_output = tk.Text(frame, height=14, width=100)
        self.log_output.grid(row=log_row, column=1, columnspan=2, sticky="nsew", pady=4)

        frame.columnconfigure(1, weight=1)
        frame.rowconfigure(log_row, weight=1)

    def _pick_callsign_file(self) -> None:
        selected = filedialog.askopenfilename(title="Select Callsign File")
        if selected:
            self.vars["callsign_file"].set(selected)

    def _pick_output_dir(self) -> None:
        selected = filedialog.askdirectory(title="Select Output Directory")
        if selected:
            self.vars["output_dir"].set(selected)

    def _log(self, msg: str) -> None:
        self.log_output.insert(tk.END, msg + "\n")
        self.log_output.see(tk.END)
        self.update_idletasks()

    def _collect_values(self) -> dict[str, str]:
        values = {k: v.get().strip() for k, v in self.vars.items()}
        values["raw_message_text"] = self.message_text.get("1.0", tk.END).strip()
        required = ["username", "password", "message_number", "station_of_origin", "place_of_origin", "date_filed", "signature", "raw_message_text", "output_dir"]
        missing = [k for k in required if not values.get(k)]
        if missing:
            raise RuntimeError("Missing required fields: " + ", ".join(missing))
        return values

    def _load_defaults(self) -> None:
        loaded = load_app_config(CONFIG_FILE)
        for key in self.vars:
            self.vars[key].set(loaded.get(key, DEFAULTS.get(key, "")))
        self.message_text.delete("1.0", tk.END)
        self.message_text.insert("1.0", loaded.get("raw_message_text", DEFAULTS["raw_message_text"]))
        self._log(f"Loaded config from {CONFIG_FILE}.")

    def _save_defaults(self) -> None:
        try:
            values = self._collect_values()
            save_app_config(CONFIG_FILE, values)
            self._log(f"Saved config to {CONFIG_FILE}.")
        except (RuntimeError, OSError) as exc:
            messagebox.showerror("Save Failed", str(exc))

    def _generate(self, callsigns: list[str]) -> None:
        try:
            values = self._collect_values()
            generated, skipped = generate_bulk(values, callsigns, self._log)
            self._log(f"Done. Generated: {generated}, Skipped: {skipped}")
            messagebox.showinfo("Complete", f"Generated: {generated}\nSkipped: {skipped}")
        except (RuntimeError, requests.RequestException, ET.ParseError, OSError) as exc:
            messagebox.showerror("Generation Failed", str(exc))
            self._log(f"ERROR: {exc}")

    def _generate_from_file(self) -> None:
        try:
            callsign_file = self.vars["callsign_file"].get().strip()
            if not callsign_file:
                raise RuntimeError("Callsign file path is required.")
            callsigns = read_callsigns_from_file(Path(callsign_file))
            self._generate(callsigns)
        except (RuntimeError, OSError) as exc:
            messagebox.showerror("Input Error", str(exc))

    def _generate_from_inline(self) -> None:
        callsigns = read_callsigns_from_text(self.inline_callsigns.get("1.0", tk.END))
        self._generate(callsigns)


def run_cli() -> None:
    values = load_app_config(CONFIG_FILE)
    if not values.get("username") or not values.get("password"):
        raise RuntimeError("config.ini must include [QRZ] username and password.")
    callsigns = read_callsigns_from_file(Path(values["callsign_file"]))

    def printer(msg: str) -> None:
        print(msg)

    generated, skipped = generate_bulk(values, callsigns, printer)
    print(f"Done. Generated: {generated}, Skipped: {skipped}")


def main() -> None:
    parser = argparse.ArgumentParser(description="Bulk Radiogram Generator")
    parser.add_argument("--cli", action="store_true", help="Run headless CLI mode")
    args = parser.parse_args()

    if args.cli:
        run_cli()
        return

    app = RadiogramGui()
    app.mainloop()


if __name__ == "__main__":
    main()
