#!/usr/bin/env python3
"""Create a Cloudflare Logpush job for IO River traffic reporting.

This script is standalone and talks directly to Cloudflare's public API.
"""

from __future__ import annotations

import argparse
import json
import logging
import os
import re
import ssl
import sys
import urllib.error
import urllib.parse
import urllib.request
from typing import Any

API_BASE = "https://api.cloudflare.com/client/v4"
logger = logging.getLogger("create_cloudflare_logpush")

FIELD_NAMES = [
    "RayID",
    "CacheCacheStatus",
    "ClientASN",
    "ClientCountry",
    "ClientIP",
    "ClientRequestHost",
    "ClientRequestMethod",
    "ClientRequestPath",
    "ClientRequestProtocol",
    "ClientRequestURI",
    "EdgeEndTimestamp",
    "EdgeStartTimestamp",
    "EdgeResponseBytes",
    "EdgeResponseStatus",
]


class CloudflareApiError(RuntimeError):
    """Raised when Cloudflare API returns a non-success response."""


def optional_certifi_cafile() -> str | None:
    try:
        import certifi  # type: ignore

        return certifi.where()
    except ImportError:
        return None


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Create a Cloudflare Logpush job."
    )
    parser.add_argument(
        "domain",
        help="Cloudflare-managed zone name (for example: example.com)",
    )
    parser.add_argument(
        "--api-token",
        default=os.getenv("CLOUDFLARE_API_TOKEN"),
        help="Cloudflare API token (or set CLOUDFLARE_API_TOKEN).",
    )
    parser.add_argument(
        "--api-email",
        default=os.getenv("CLOUDFLARE_API_EMAIL"),
        help="Cloudflare account email for Global API key auth.",
    )
    parser.add_argument(
        "--api-key",
        default=os.getenv("CLOUDFLARE_API_KEY"),
        help="Cloudflare Global API key (or set CLOUDFLARE_API_KEY).",
    )
    parser.add_argument(
        "--account-provider-id",
        required=True,
        help="IO River account_provider_id used in the Logpush destination URL.",
    )
    parser.add_argument(
        "--job-name",
        default="Send sample logs to IO River",
        help="Logpush job name shown in Cloudflare.",
    )
    parser.add_argument(
        "--destination-base-url",
        default="https://traffic-reporting.ioriver.io/public/cloudflare/unified",
        help="Base destination URL before /<account_provider_id>.",
    )
    parser.add_argument(
        "--ownership-challenge",
        default="",
        help="Optional ownership challenge token, if your destination requires it.",
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="Print payload and exit without creating a Logpush job.",
    )
    parser.add_argument(
        "--ca-bundle",
        default=os.getenv("SSL_CERT_FILE") or os.getenv("REQUESTS_CA_BUNDLE"),
        help="Path to a PEM CA bundle to use for HTTPS verification.",
    )
    parser.add_argument(
        "--insecure",
        action="store_true",
        help="Disable TLS certificate verification. Use only for debugging.",
    )
    parser.add_argument(
        "--verbose",
        action="store_true",
        help="Enable debug logging for Cloudflare API requests and responses.",
    )

    args = parser.parse_args()
    validate_auth(args)
    return args


def validate_auth(args: argparse.Namespace) -> None:
    has_token = bool(args.api_token)
    has_key_pair = bool(args.api_email and args.api_key)
    if not (has_token or has_key_pair):
        raise SystemExit(
            "Authentication required: provide --api-token (preferred) "
            "or both --api-email and --api-key."
        )


def build_headers(args: argparse.Namespace) -> dict[str, str]:
    headers: dict[str, str] = {
        "Content-Type": "application/json",
        "Accept": "application/json",
    }
    if args.api_token:
        headers["Authorization"] = f"Bearer {args.api_token}"
    else:
        headers["X-Auth-Email"] = args.api_email
        headers["X-Auth-Key"] = args.api_key
    return headers


def configure_logging(verbose: bool) -> None:
    level = logging.DEBUG if verbose else logging.INFO
    logging.basicConfig(
        level=level,
        format="%(asctime)s %(levelname)s %(name)s: %(message)s",
    )


def redact_headers(headers: dict[str, str]) -> dict[str, str]:
    redacted = dict(headers)
    if "Authorization" in redacted:
        redacted["Authorization"] = "Bearer [redacted]"
    if "X-Auth-Key" in redacted:
        redacted["X-Auth-Key"] = "[redacted]"
    return redacted


def build_ssl_context(args: argparse.Namespace) -> ssl.SSLContext | None:
    if args.insecure:
        logger.warning("TLS certificate verification disabled via --insecure")
        context = ssl.create_default_context()
        context.check_hostname = False
        context.verify_mode = ssl.CERT_NONE
        return context

    cafile = args.ca_bundle or optional_certifi_cafile()
    if cafile:
        logger.info("Using CA bundle: %s", cafile)
        return ssl.create_default_context(cafile=cafile)

    logger.info("Using system default CA certificates")
    return ssl.create_default_context()


def normalize_job_name(job_name: str) -> str:
    normalized = re.sub(r"[^a-zA-Z0-9._-]+", "_", job_name).strip("._-")
    return normalized or "ioriver-logpush"


def api_request(
    method: str,
    path: str,
    headers: dict[str, str],
    ssl_context: ssl.SSLContext | None,
    payload: dict[str, Any] | None = None,
) -> dict[str, Any]:
    data: bytes | None = None
    if payload is not None:
        data = json.dumps(payload).encode("utf-8")

    logger.debug("Cloudflare API request: %s %s", method, path)
    logger.debug("Cloudflare API request headers: %s", redact_headers(headers))
    if payload is not None:
        logger.debug("Cloudflare API request payload: %s", json.dumps(payload, indent=2))

    req = urllib.request.Request(
        url=f"{API_BASE}/{path.lstrip('/')}",
        data=data,
        headers=headers,
        method=method,
    )
    try:
        with urllib.request.urlopen(req, timeout=30, context=ssl_context) as resp:
            logger.debug("Cloudflare API response status: %s %s", resp.status, resp.reason)
            logger.debug("Cloudflare API response headers: %s", dict(resp.headers.items()))
            raw = resp.read().decode("utf-8")
    except urllib.error.HTTPError as err:
        body = err.read().decode("utf-8", errors="replace")
        logger.error(
            "Cloudflare API HTTPError: method=%s path=%s status=%s reason=%s",
            method,
            path,
            err.code,
            err.reason,
        )
        logger.error("Cloudflare API HTTPError headers: %s", dict(err.headers.items()))
        logger.error("Cloudflare API HTTPError body: %s", body)
        raise CloudflareApiError(
            f"Cloudflare API HTTP {err.code} for {path}: {body}"
        ) from err
    except urllib.error.URLError as err:
        logger.error(
            "Cloudflare API URLError: method=%s path=%s reason=%s",
            method,
            path,
            err.reason,
        )
        raise CloudflareApiError(f"Network error calling Cloudflare API: {err}") from err

    try:
        parsed = json.loads(raw)
    except json.JSONDecodeError as err:
        logger.error("Cloudflare API returned invalid JSON for %s: %s", path, raw)
        raise CloudflareApiError(
            f"Cloudflare API returned non-JSON response for {path}: {raw}"
        ) from err

    if not parsed.get("success", False):
        logger.error("Cloudflare API returned success=false for %s: %s", path, json.dumps(parsed, indent=2))
        raise CloudflareApiError(
            f"Cloudflare API call failed for {path}: {json.dumps(parsed, indent=2)}"
        )

    logger.debug("Cloudflare API response payload: %s", json.dumps(parsed, indent=2))
    return parsed


def get_zone_id(
    domain: str,
    headers: dict[str, str],
    ssl_context: ssl.SSLContext | None,
) -> str:
    params = urllib.parse.urlencode({"name": domain, "per_page": 50})
    logger.info("Looking up Cloudflare zone for domain: %s", domain)
    response = api_request("GET", f"zones?{params}", headers=headers, ssl_context=ssl_context)
    zones = response.get("result", [])
    logger.debug("Zone lookup returned %d result(s)", len(zones))

    for zone in zones:
        if str(zone.get("name", "")).lower() == domain.lower():
            return str(zone["id"])

    if zones:
        names = ", ".join(str(z.get("name")) for z in zones)
        raise CloudflareApiError(
            f"No exact zone match for domain '{domain}'. Candidates returned: {names}"
        )

    raise CloudflareApiError(
        f"No Cloudflare zone found for domain '{domain}'. Check token scope and domain."
    )


def build_logpush_payload(args: argparse.Namespace) -> dict[str, Any]:
    destination = (
        f"{args.destination_base_url.rstrip('/')}/{args.account_provider_id.strip()}"
    )
    job_name = normalize_job_name(args.job_name)
    if job_name != args.job_name:
        logger.warning(
            "Cloudflare job name %r was normalized to %r to satisfy API validation",
            args.job_name,
            job_name,
        )

    filter_obj = {
        "where": {
            "key": "ClientRequestSource",
            "operator": "eq",
            "value": "eyeball",
        }
    }

    payload: dict[str, Any] = {
        "name": job_name,
        "destination_conf": destination,
        "dataset": "http_requests",
        "enabled": True,
        "filter": json.dumps(filter_obj),
        "output_options": {
            "field_names": FIELD_NAMES,
            "timestamp_format": "unixnano",
            "sample_rate": 0.01,
        },
    }

    if args.ownership_challenge:
        payload["ownership_challenge"] = args.ownership_challenge

    return payload


def main() -> int:
    args = parse_args()
    configure_logging(args.verbose)
    headers = build_headers(args)
    ssl_context = build_ssl_context(args)

    try:
        zone_id = get_zone_id(args.domain, headers, ssl_context)
        payload = build_logpush_payload(args)

        print(f"Resolved zone '{args.domain}' to zone_id={zone_id}")
        print("Logpush payload:")
        print(json.dumps(payload, indent=2))
        logger.info("Prepared Logpush job payload for domain=%s zone_id=%s", args.domain, zone_id)

        if args.dry_run:
            print("Dry run enabled. No changes were made.")
            logger.info("Dry run requested; skipping job creation")
            return 0

        response = api_request(
            "POST",
            f"zones/{zone_id}/logpush/jobs",
            headers=headers,
            ssl_context=ssl_context,
            payload=payload,
        )

        result = response.get("result", {})
        logger.info("Created Logpush job successfully: job_id=%s", result.get("id"))
        print("Created Logpush job successfully.")
        print(f"job_id={result.get('id')}")
        print(f"dataset={result.get('dataset')}")
        print(f"destination_conf={result.get('destination_conf')}")
        return 0
    except CloudflareApiError as err:
        logger.exception("Cloudflare Logpush creation failed")
        print(f"ERROR: {err}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    sys.exit(main())
