import httpx
import asyncio
import os
from typing import Any
from pathlib import Path
from rich.table import Table
from rich.panel import Panel
from rich.prompt import Prompt, IntPrompt
from .common import console, p_success, p_error, p_warning, debug

HEADERS = {
    "accept": "application/json",
    "content-type": "application/json",
}
STORAGE_REGIONS = ["DE", "UK", "SE", "NY", "LA", "SG", "SYD", "BR", "JH"]


# price per GB as of Nov 2025
PULL_GEO_ZONES = {
    "ASIA": 0.03,
    "EU": 0.01,
    "AF": 0.06,
    "NA": 0.01,
    "SA": 0.045,
}
GIGABYTE = 1_000_000_000


client = httpx.Client(headers=HEADERS, timeout=60)
aclient = httpx.AsyncClient(headers=HEADERS, timeout=60)


class BunnyError(Exception):
    pass


async def get_full_storage_zone(
    config: dict[str, str], dir: str
) -> list[tuple[str, str]]:
    """
    Get list of all files in storage zone by subdir.
    Recursively calls self to build complete list.

    Returns: list of (filename, checksum) tuples for each file in zone.
    """
    region = config["bunny"]["region"]
    zone = config["bunny"]["storage_zone"]
    password = config["bunny"]["password"]

    url = f"https://{_region_prefix(region)}storage.bunnycdn.com/{zone}/{dir}/"
    response = await aclient.get(url, headers={"accessKey": password})

    directories = []
    files = []

    for obj in response.json():
        full_name = obj["Path"].split("/", 2)[2] + obj["ObjectName"]
        if obj["IsDirectory"]:
            directories.append(full_name)
        else:
            files.append((full_name, obj["Checksum"]))

    tasks = [get_full_storage_zone(config, d) for d in directories]
    subdirs = await asyncio.gather(*tasks)
    for result in subdirs:
        files.extend(result)

    return files


def _region_prefix(region: str) -> str:
    return "" if region == "DE" else f"{region.lower()}."


def _path_url(config: dict[str, Any], path: str) -> str:
    if config["remote_path"]:
        full_path = f"{config['remote_path']}/{path}"
    else:
        full_path = path
    zone = config["bunny"]["storage_zone"]
    return f"https://{_region_prefix(config['bunny']['region'])}storage.bunnycdn.com/{zone}/{full_path}"


async def upload_file(
    config: dict[str, Any],
    path: str,
    local_dir: Path,
) -> None:
    """
    upload a file

    called by publish
    """
    url = _path_url(config, path)
    with open(local_dir / path, "rb") as file:
        resp = await aclient.put(
            url,
            headers=HEADERS | {"AccessKey": config["bunny"]["password"]},
            data=file.read(),
        )
        # expect 201
        if resp.status_code != 201:
            debug(resp)  # 201s
            resp.raise_for_status()


async def delete_remote_file(config: dict[str, Any], path: Path) -> None:
    """
    delete a remote file

    called by publish (--delete)
    """
    url = _path_url(config, path)
    resp = await aclient.delete(
        url, headers=HEADERS | {"AccessKey": config["bunny"]["password"]}
    )
    if resp.status_code != 200:
        debug(resp)
        resp.raise_for_status()


def _list_storage_zones(bunny_key: str) -> dict[str, Any]:
    """
    Get all storage zones (called by zones)
    """
    url = "https://api.bunny.net/storagezone?page=0&perPage=1000&includeDeleted=false"
    zones = client.get(url, headers=HEADERS | {"AccessKey": bunny_key}).json()
    return zones


def _get_storage_zone_by_name(key: str, name: str) -> dict:
    """
    Look up a specific storage zone by name, useful for obtaining password.
    """
    for zone in _list_storage_zones(key):
        if zone["Name"] == name:
            return zone


def augment_config(config: dict[str, str]) -> None:
    config["bunny"]["api_key"] = _api_key()
    zone = _get_storage_zone_by_name(
        config["bunny"]["api_key"], config["bunny"]["storage_zone"]
    )
    config["bunny"]["region"] = zone["Region"]
    config["bunny"]["password"] = zone["Password"]
    if len(zone["PullZones"]) > 1:
        p_warning("multiple pull zones, using first")  # TODO; allow config

    # TODO: allow customization
    config["custom_404_page"] = zone["Custom404FilePath"]
    config["bunny"]["storage_zone_id"] = zone["Id"]
    config["bunny"]["pull_zone_id"] = zone["PullZones"][0]["Id"]

    redirects = []
    for rule in zone["PullZones"][0]["EdgeRules"]:
        # redirect
        if rule["ActionType"] == 1 and rule["Description"].startswith("trifold"):
            redirects.append(
                {
                    "guid": rule["Guid"],
                    "to": rule["ActionParameter1"],
                    "from": rule["Triggers"][0]["PatternMatches"][0],
                }
            )
    config["bunny"]["existing_redirects"] = redirects


def hint(hint_str: str) -> None:
    console.print(Panel(hint_str, style="#cccccc"))


def _api_key() -> str:
    bunny_api_key = os.getenv("BUNNY_API_KEY")
    if not bunny_api_key:
        hint("can also be provided as env variable BUNNY_API_KEY")
        bunny_api_key = Prompt.ask("Bunny API Key")
    return bunny_api_key


def _api_post(method: str, payload: dict[str, str], key: str) -> httpx.Response:
    """
    make a bunny.net API POST
    """
    url = "https://api.bunny.net/" + method
    resp = client.post(url, json=payload, headers=HEADERS | {"accessKey": key})
    if resp.status_code >= 400:
        raise BunnyError(resp.text)
    return resp


def _create_storage_zone(
    bunny_key: str, name: str, region: str, replication: list[str]
) -> httpx.Response:
    """
    create a bunny.net storage zone (bucket)
    """
    payload = {
        "Name": name,
        "Region": region,
        "ReplicationRegions": replication,
        "ZoneTier": "Standard",
    }
    return _api_post("storagezone", payload, bunny_key)


def _create_pull_zone(
    bunny_key: str, storage_zone_id: str, name: str, limit_gb: int, cache_seconds: int
) -> httpx.Response:
    """
    create a bunny.net pull zone (CDN hostname)
    """
    payload = {
        "Name": name,
        # TODO: make configurable
        "Type": "Premium",  # best for sites? Volume does not have geo zones?
        "EnableGeoZoneUS": True,
        "EnableGeoZoneEU": False,
        "EnableGeoZoneASIA": False,
        "EnableGeoZoneSA": False,
        "EnableGeoZoneAF": False,
        "BlockPostRequests": False,
        "MonthlyBandwidthLimit": limit_gb * GIGABYTE,
        "EnableAutoSSL": True,
        "StorageZoneId": storage_zone_id,
        "OriginType": 2,  # Storage Zone origin
        "PreloadingScreenEnabled": False,
        "CacheControlPublicMaxAgeOverride": cache_seconds,
        # EnableLogging?,
    }
    return _api_post("pullzone", payload, bunny_key).json()


def add_custom_hostname(config: dict[str, str], hostname: str) -> None:
    """
    add a hostname and initialize SSL
    """
    bunny_key = config["bunny"]["api_key"]
    pull_zone_id = config["bunny"]["pull_zone_id"]
    try:
        resp = _api_post(
            f"pullzone/{pull_zone_id}/addHostname", {"Hostname": hostname}, bunny_key
        )
        p_success(f"hostname '{hostname}' added")
    except BunnyError as e:
        if "hostname_already_registered" in e.args[0]:
            p_warning(f"hostname '{hostname}' already exists")
        else:
            raise
    url = f"https://api.bunny.net/pullzone/loadFreeCertificate?hostname={hostname}"
    try:
        resp = client.get(url, headers=HEADERS | {"AccessKey": bunny_key})
        if resp.status_code != 200:
            p_error(f"failed to obtain certificate: {resp.json()}")
    except httpx.ReadTimeout:
        p_error("""Certificate request timed out. Sometimes this happens?
Proceeding, if next call fails, try again in a few seconds.""")

    try:
        _api_post(
            f"pullzone/{pull_zone_id}/setForceSSL",
            {"Hostname": hostname, "ForceSSL": True},
            bunny_key,
        )
    except BunnyError as e:
        if "hostname_ssl_not_enabled" in e.args[0]:
            p_error("Certificate pending, try again in a few seconds.")
            return
        else:
            raise
    p_success("SSL enabled!")


def _show_prices_table(key: str) -> None:
    """
    show available regions (limited to those known to be valid from web UI)
    """
    resp = client.get("https://api.bunny.net/region", headers={"AccessKey": key})
    table = Table(show_header=True, header_style="bold")
    table.add_column("Region")
    # prices in API are not for storage costs, do not use
    # table.add_column("Price/GB")
    table.add_column("Name")
    for region in resp.json():
        if region["RegionCode"] in STORAGE_REGIONS:
            table.add_row(region["RegionCode"], region["Name"])
    console.print(table)


def extra_init() -> dict[str, str]:
    bunny_api_key = _api_key()
    hint("valid characters \\[a-z0-9-] ; will be used as subdomain.b-cdn.net")
    zone_name = Prompt.ask("storage zone name")

    # TODO: SSD storage option (no regions, 0.08/GB, much faster?)

    _show_prices_table(bunny_api_key)
    hint("where your files will be stored")
    region = Prompt.ask("region", choices=STORAGE_REGIONS, default="DE")
    # prompt for replication regions
    replication_regions = []
    while True:
        rep_region = Prompt.ask(
            "add replication region? (blank to stop)",
            choices=STORAGE_REGIONS + [""],
            default="",
        )
        if rep_region:
            replication_regions.append(rep_region)
        else:
            break

    # storage zone
    szone = _get_storage_zone_by_name(bunny_api_key, zone_name)

    if szone:
        p_error(f"storage zone '{zone_name}' already exists, exiting")
        console.print(f"  id: {szone['Id']}")
        console.print(f"  region: {szone['Region']}")
        console.print(f"  replication regions: {szone['ReplicationRegions']}")
        return -1

    # pull zone details
    max_gb = IntPrompt.ask("Monthly bandwidth limit (GB): ", default=10)
    browser_cache_mins = IntPrompt.ask("Browser cache time (mins): ", default=12 * 60)
    seconds = browser_cache_mins * 60

    # do creation
    _create_storage_zone(bunny_api_key, zone_name, region, replication_regions)
    p_success(f"created storage zone '{zone_name}'")
    szone = _get_storage_zone_by_name(bunny_api_key, zone_name)
    _create_pull_zone(bunny_api_key, szone["Id"], zone_name, max_gb, seconds)
    p_success(f"created pull zone '{zone_name}'")

    return {
        "storage_zone": zone_name,
    }


def set_404_page(config: dict[str, Any], path: str) -> None:
    _api_post(
        f"storagezone/{config['bunny']['storage_zone_id']}/",
        {"Custom404FilePath": path},
        config["bunny"]["api_key"],
    )


def purge_cache(config: dict[str, str]) -> None:
    _api_post(
        f"pullzone/{config['bunny']['pull_zone_id']}/purgeCache",
        {},
        config["bunny"]["api_key"],
    )


def get_cdn_url(config: dict[str, str]) -> str:
    return config["bunny"]["storage_zone"] + ".b-cdn.net"


def show_sites() -> None:
    key = _api_key()
    existing_zones = _list_storage_zones(key)
    table = Table(show_header=True, header_style="bold")
    table.add_column("zone")
    table.add_column("regions")
    table.add_column("storage (MB)")
    table.add_column("bandwidth (GB)")
    table.add_column("cost ($)")
    table.add_column("hostname")
    for zone in existing_zones:
        for pullzone in zone["PullZones"]:
            hostnames = []
            for host in pullzone["Hostnames"]:
                hostnames.append(
                    ("https://" if host["ForceSSL"] else "http://") + host["Value"]
                )
            # if custom hostname set, hide default
            if len(hostnames) > 1:
                hostnames = [h for h in hostnames if "b-cdn.net" not in h]
            table.add_row(
                zone["Name"],
                zone["Region"] + " " + "; ".join(zone["ReplicationRegions"]),
                f"{zone['StorageUsed'] / 1000**2:.0f}",
                f"{pullzone['MonthlyBandwidthUsed'] / GIGABYTE:.0f} / "
                f"{pullzone['MonthlyBandwidthLimit'] / GIGABYTE:.0f}",
                f"{pullzone['MonthlyCharges']:.4f}",
                "\n".join(hostnames),
            )
    console.print(table)


def _create_redirect(
    from_pattern: str, to_pattern: str, guid: str, config: dict[str, Any]
) -> bool:
    method = f"/pullzone/{config['bunny']['pull_zone_id']}/edgerules/addOrUpdate"
    payload = {
        "Triggers": [
            {"Type": "Url", "PatternMatches": [from_pattern], "PatternMatchingType": 1}
        ],
        "TriggerMatchingType": 1,
        "Description": f"trifold {from_pattern}",
        "Enabled": True,
        "ActionType": "Redirect",
        "ActionParameter1": to_pattern,
    }
    if guid:
        payload["Guid"] = guid

    _api_post(method, payload, config["bunny"]["api_key"])


def create_redirects(config: dict[str, Any]) -> None:
    from_rule_guid = {
        r["from"]: (r["guid"], r["to"]) for r in config["bunny"]["existing_redirects"]
    }

    for r in config.get("redirects", []):
        guid = None
        if exist := from_rule_guid.get(r["from"]):
            guid, to = exist
            if to == r["to"]:
                p_warning(f"no change for {r['from']} => {r['to']}")
                continue
            p_success(f"updating {r['from']} => {r['to']}")
            _create_redirect(r["from"], r["to"], guid, config)
        else:
            p_success(f"adding {r['from']} => {r['to']}")
            _create_redirect(r["from"], r["to"], None, config)
