from datetime import datetime, timedelta, timezone
from pathlib import Path
import time

from .product_definitions import PRODUCT_DEFINITIONS, get_product_definition

try:
    import requests
except ModuleNotFoundError:  # pragma: no cover - dependency guard for runtime environments
    requests = None

PROJECT_ROOT = Path(__file__).resolve().parents[2]
OUTPUT_DIR = PROJECT_ROOT / "data" / "display" / "grib"
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

REQUEST_TIMEOUT = (10, 120)
RETRY_LIMIT = 3
MIN_FILE_SIZE_BYTES = 1024
DEFAULT_FORECAST_HOURS = [
    0, 3, 6, 9, 12, 15, 18, 21, 24,
    27, 30, 33, 36, 39, 42, 45,
    48, 51, 54, 57, 60, 63, 66,
    69, 72,
]
def get_cycle(reference_time=None):
    now = reference_time or datetime.now(timezone.utc)
    candidate = now - timedelta(hours=5)
    hour = (candidate.hour // 6) * 6
    cycle_time = candidate.replace(hour=hour, minute=0, second=0, microsecond=0)
    return cycle_time.strftime("%Y%m%d"), f"{cycle_time.hour:02d}"


def is_fatal_network_error(error):
    error_text = str(error)
    fatal_markers = (
        "NameResolutionError",
        "Failed to resolve",
        "Temporary failure in name resolution",
    )
    return any(marker in error_text for marker in fatal_markers)


def validate_response(response):
    content_type = response.headers.get("Content-Type", "").lower()
    if "html" in content_type or "text/plain" in content_type:
        preview = response.text[:200].strip().replace("\n", " ")
        raise ValueError(f"unexpected response content type {content_type}: {preview}")


def build_request(definition, date, cycle, forecast_hour):
    download = definition["download"]
    params = {
        "file": download["file_template"].format(cycle=cycle, forecast_hour=forecast_hour),
        "dir": download["dir_template"].format(date=date, cycle=cycle),
    }

    region = download.get("region")
    if region:
        params.update(region)

    for variable in download["variables"]:
        params[f"var_{variable}"] = "on"
    for level in download["levels"]:
        params[level] = "on"
    return params


def output_path_for(definition, date, cycle, forecast_hour):
    suffix = definition["download"]["suffix"]
    return OUTPUT_DIR / f"{date}_{cycle}_f{forecast_hour}{suffix}"


def download_file(session, base_url, params, output_path):
    temp_path = output_path.with_name(f"{output_path.name}.part")

    if output_path.exists() and output_path.stat().st_size >= MIN_FILE_SIZE_BYTES:
        print("skip", output_path)
        return

    for attempt in range(1, RETRY_LIMIT + 1):
        try:
            print(f"downloading {output_path} (attempt {attempt}/{RETRY_LIMIT})")
            with session.get(base_url, params=params, stream=True, timeout=REQUEST_TIMEOUT) as response:
                response.raise_for_status()
                validate_response(response)
                with temp_path.open("wb") as file_handle:
                    for chunk in response.iter_content(1024 * 1024):
                        if chunk:
                            file_handle.write(chunk)

            if temp_path.stat().st_size < MIN_FILE_SIZE_BYTES:
                raise ValueError(f"downloaded file too small: {temp_path.stat().st_size} bytes")

            temp_path.replace(output_path)
            return
        except (requests.RequestException, ValueError) as exc:
            if temp_path.exists():
                temp_path.unlink()
            print("  download failed:", exc)
            if is_fatal_network_error(exc):
                raise RuntimeError("fatal network error while reaching NOAA") from exc
            if attempt == RETRY_LIMIT:
                raise
            time.sleep(attempt * 2)


def download_product(session, product, date, cycle, forecast_hour):
    if product == "rain" and forecast_hour == "000":
        print("skip empty source for rain forecast 000")
        return None

    definition = get_product_definition(product)
    params = build_request(definition, date, cycle, forecast_hour)
    output_path = output_path_for(definition, date, cycle, forecast_hour)
    download_file(session, definition["download"]["base_url"], params, output_path)
    return output_path


def download_cycle(products=None, forecast_hours=None, reference_time=None):
    if requests is None:
        raise RuntimeError("requests is required to download display source data")

    date, cycle = get_cycle(reference_time=reference_time)
    print("display cycle:", date, cycle)

    selected_products = products or list(PRODUCT_DEFINITIONS.keys())
    selected_hours = forecast_hours or [f"{hour:03d}" for hour in DEFAULT_FORECAST_HOURS]

    session = requests.Session()
    session.headers["User-Agent"] = "weather-display/1.0"

    failures = []
    downloads = []
    for forecast_hour in selected_hours:
        for product in selected_products:
            try:
                output_path = download_product(session, product, date, cycle, forecast_hour)
                if output_path is not None:
                    downloads.append(output_path)
            except RuntimeError as exc:
                failures.append((product, forecast_hour, str(exc)))
                break
            except Exception as exc:
                failures.append((product, forecast_hour, str(exc)))
        if failures and failures[-1][2].startswith("fatal network error"):
            break

    if failures:
        for product, forecast_hour, error in failures:
            print(f"failed {product} forecast {forecast_hour}: {error}")
        raise SystemExit(1)

    return downloads


def main():
    download_cycle()


if __name__ == "__main__":
    main()
