"""Manually refresh the local phone-country picker. Never called by the website.

Network is used only by this maintenance script; generated PHP and SVG files are
self-contained. With no arguments the pinned revisions in the source manifest
are reproduced. --latest explicitly refreshes each upstream revision.
"""
from __future__ import annotations

import argparse
import datetime
import hashlib
import http.client
import io
import json
import re
import urllib.request
import xml.etree.ElementTree as ET
import zipfile
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
MANIFEST = ROOT / "docs" / "phone-countries.sources.json"
REPOS = {
    "numbers": ("google/libphonenumber", "master"),
    "names": ("unicode-org/cldr-json", "main"),
    "flags": ("lipis/flag-icons", "main"),
}
FLAG_SOURCE_CODES = {"ac": "sh-ac", "ta": "sh-ta"}


def download(url: str) -> bytes:
    request = urllib.request.Request(url, headers={"User-Agent": "LaFleur-local-phone-data-updater/1.0"})
    for attempt in range(3):
        try:
            with urllib.request.urlopen(request, timeout=60) as response:
                return response.read()
        except (OSError, http.client.HTTPException):
            if attempt == 2:
                raise
    raise RuntimeError("Download did not complete")


def php_string(value: str) -> str:
    return "'" + value.replace("\\", "\\\\").replace("'", "\\'") + "'"


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--latest", action="store_true", help="Resolve latest upstream commits instead of reproducing the pinned snapshot")
    arguments = parser.parse_args()
    old = json.loads(MANIFEST.read_text(encoding="utf-8")) if MANIFEST.exists() else None
    sources: dict[str, dict[str, str]] = {}
    for key, (repo, branch) in REPOS.items():
        revision = (old or {}).get("sources", {}).get(key, {}).get("revision")
        if arguments.latest or not revision:
            revision = json.loads(download(f"https://api.github.com/repos/{repo}/commits/{branch}"))["sha"]
        sources[key] = {"repository": repo, "revision": revision}

    def raw(key: str, path: str) -> bytes:
        source = sources[key]
        return download(f"https://raw.githubusercontent.com/{source['repository']}/{source['revision']}/{path}")

    metadata = raw("numbers", "resources/PhoneNumberMetadata.xml")
    names_content = raw("names", "cldr-json/cldr-localenames-full/main/ru/territories.json")
    names = json.loads(names_content)["main"]["ru"]["localeDisplayNames"]["territories"]
    countries = {}
    for territory in ET.fromstring(metadata).findall("./territories/territory"):
        iso = territory.attrib["id"]
        # 001 represents non-geographic services, rather than a country/flag.
        if not re.fullmatch(r"[A-Z]{2}", iso):
            continue
        dial = territory.attrib["countryCode"]
        if iso not in names or not re.fullmatch(r"[1-9][0-9]{0,2}", dial):
            raise ValueError(f"Missing Russian name or invalid calling code: {iso}")
        countries[iso.lower()] = {"name": names[iso], "dial": dial}
    if len(countries) < 230:
        raise ValueError("Unexpected upstream country coverage: refusing a partial update")

    flag_source = sources["flags"]
    archive_content = download(f"https://codeload.github.com/{flag_source['repository']}/zip/{flag_source['revision']}")
    flag_files: dict[str, bytes] = {}
    with zipfile.ZipFile(io.BytesIO(archive_content)) as archive:
        prefix = archive.namelist()[0]
        for iso in countries:
            source_iso = FLAG_SOURCE_CODES.get(iso, iso)
            svg = archive.read(f"{prefix}flags/4x3/{source_iso}.svg")
            tree = ET.fromstring(svg)
            if tree.tag != "{http://www.w3.org/2000/svg}svg":
                raise ValueError(f"Invalid SVG root: {iso}")
            for element in tree.iter():
                if element.tag.rsplit("}", 1)[-1] in {"script", "foreignObject", "image"}:
                    raise ValueError(f"Unexpected executable or external SVG content: {iso}")
                for attribute, value in element.attrib.items():
                    if attribute.lower().startswith("on") or (attribute.rsplit("}", 1)[-1] == "href" and not value.startswith("#")):
                        raise ValueError(f"Unexpected SVG reference: {iso}")
            flag_files[iso] = svg
        flag_license = archive.read(f"{prefix}LICENSE")

    # Collect all network-dependent input before changing any local files.
    licenses = {
        "google-libphonenumber-LICENSE.txt": raw("numbers", "LICENSE"),
        "unicode-CLDR-LICENSE.txt": raw("names", "LICENSE"),
        "flag-icons-LICENSE.txt": flag_license,
    }
    flag_directory = ROOT / "public" / "assets" / "flags"
    flag_directory.mkdir(parents=True, exist_ok=True)
    license_directory = ROOT / "docs" / "licenses"
    license_directory.mkdir(parents=True, exist_ok=True)
    for name, contents in licenses.items():
        (license_directory / name).write_bytes(contents)
    for iso, contents in flag_files.items():
        (flag_directory / f"{iso}.svg").write_bytes(contents)

    ordered = sorted(countries.items(), key=lambda entry: entry[1]["name"].casefold().replace("ё", "е"))
    lines = [
        "<?php", "declare(strict_types=1);", "",
        "/** Local country calling-code data. See docs/phone-countries.md for sources. */",
        "final class PhoneCountries", "{",
        "    /** @return array<string, array{name: string, dial: string, flag: string}> */",
        "    public static function all(): array", "    {", "        return [",
    ]
    for iso, country in ordered:
        fields = ", ".join(f"{php_string(key)} => {php_string(value)}" for key, value in [*country.items(), ("flag", f"/public/assets/flags/{iso}.svg")])
        lines.append(f"            {php_string(iso)} => [{fields}],")
    lines += ["        ];", "    }", "}", ""]
    (ROOT / "app" / "PhoneCountries.php").write_text("\n".join(lines), encoding="utf-8")
    manifest = {
        "retrieved_utc": datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds"),
        "countries": len(countries),
        "sources": sources,
        "flag_source_code_aliases": FLAG_SOURCE_CODES,
        "input_sha256": {"PhoneNumberMetadata.xml": hashlib.sha256(metadata).hexdigest(), "ru-territories.json": hashlib.sha256(names_content).hexdigest()},
        "flags_sha256": {f"{iso}.svg": hashlib.sha256(contents).hexdigest() for iso, contents in sorted(flag_files.items())},
    }
    MANIFEST.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    print(f"Generated {len(countries)} countries, {len(flag_files)} local SVG flags, and upstream licenses.")


if __name__ == "__main__":
    main()
