from __future__ import annotations

import argparse
from dataclasses import dataclass
from datetime import datetime, timezone
import json
import os
from pathlib import Path
from typing import Any
from urllib.parse import quote

import requests
from dotenv import load_dotenv


PROJECT_ROOT = Path(__file__).resolve().parents[1]
LOGS_DIR = PROJECT_ROOT / "logs"
DEFAULT_REPORT_PATH = LOGS_DIR / "foxylink_probe_report.json"


def utc_now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def _as_bool(raw: str | None, default: bool) -> bool:
    if raw is None:
        return default
    return raw.strip().lower() in {"1", "true", "yes", "on"}


def _normalize_path(path: str | None, default: str) -> str:
    value = (path or default).strip()
    if not value:
        value = default
    if not value.startswith("/"):
        value = "/" + value
    if not value.endswith("/"):
        value = value + "/"
    return value


def _resolve_path(raw: str) -> Path:
    path = Path(raw)
    if path.is_absolute():
        return path
    return PROJECT_ROOT / path


def _load_payload(payload_file: str | None, fallback_json: str) -> tuple[Any, str, str]:
    payload_source = "env:ONEC_FOXY_PAYLOAD_JSON"
    raw = fallback_json
    if payload_file:
        payload_path = _resolve_path(payload_file)
        payload_source = f"file:{payload_path}"
        raw = payload_path.read_text(encoding="utf-8")

    try:
        payload_obj = json.loads(raw)
        if isinstance(payload_obj, str):
            body = payload_obj
        else:
            body = json.dumps(payload_obj, ensure_ascii=False)
        return payload_obj, body, payload_source
    except json.JSONDecodeError:
        return raw, raw, payload_source


def _preview(value: str, limit: int = 2000) -> str:
    if len(value) <= limit:
        return value
    return value[:limit] + "...<truncated>"


def _classify(status_code: int | None, error: str | None) -> str:
    if error:
        return "network_error"
    if status_code is None:
        return "unknown_error"
    if status_code == 200:
        return "reachable"
    if status_code in {401, 403}:
        return "auth_failed"
    if status_code == 404:
        return "endpoint_not_found_or_not_published"
    if status_code >= 500:
        return "service_error"
    return "unexpected_status"


@dataclass(frozen=True)
class FoxyProbeSettings:
    base_url: str
    infobase: str
    username: str
    password: str
    timeout: int
    verify_tls: bool
    foxy_path: str
    exchange: str
    operation: str
    message_type: str
    payload_json: str
    reply_to: str
    app_id: str
    correlation_id: str

    @property
    def endpoint_url(self) -> str:
        base = self.base_url.rstrip("/")
        infobase = self.infobase.strip().strip("/")
        path = self.foxy_path
        exchange = quote(self.exchange.strip(), safe="")
        operation = quote(self.operation.strip(), safe="")
        message_type = quote(self.message_type.strip().upper(), safe="")
        suffix = f"{path}v1/{exchange}/{operation}/{message_type}"
        if infobase:
            return f"{base}/{infobase}{suffix}"
        return f"{base}{suffix}"


def _load_settings(args: argparse.Namespace) -> FoxyProbeSettings:
    env_file = PROJECT_ROOT / ".env"
    if env_file.exists():
        load_dotenv(env_file)

    base_url = os.getenv("ONEC_BASE_URL", "http://localhost").strip()
    infobase = os.getenv("ONEC_INFOBASE", "AccountingBase").strip()
    username = os.getenv("ONEC_USERNAME", "").strip()
    password = os.getenv("ONEC_PASSWORD", "")
    timeout = int(os.getenv("ONEC_TIMEOUT", "30").strip())
    verify_tls = _as_bool(os.getenv("ONEC_VERIFY_TLS"), default=False)

    foxy_path = _normalize_path(
        args.foxy_path or os.getenv("ONEC_FOXY_PATH", "/hs/AppEndpoint/"),
        default="/hs/AppEndpoint/",
    )
    exchange = (args.exchange or os.getenv("ONEC_FOXY_EXCHANGE", "Self")).strip()
    operation = (args.operation or os.getenv("ONEC_FOXY_OPERATION", "Query")).strip()
    message_type = (args.message_type or os.getenv("ONEC_FOXY_TYPE", "SYNC")).strip()
    payload_json = os.getenv("ONEC_FOXY_PAYLOAD_JSON", "{}")
    reply_to = (args.reply_to or os.getenv("ONEC_FOXY_REPLYTO", "")).strip()
    app_id = (args.app_id or os.getenv("ONEC_FOXY_APPID", "")).strip()
    correlation_id = (args.correlation_id or os.getenv("ONEC_FOXY_CORRELATION_ID", "")).strip()

    return FoxyProbeSettings(
        base_url=base_url,
        infobase=infobase,
        username=username,
        password=password,
        timeout=timeout,
        verify_tls=verify_tls,
        foxy_path=foxy_path,
        exchange=exchange,
        operation=operation,
        message_type=message_type,
        payload_json=payload_json,
        reply_to=reply_to,
        app_id=app_id,
        correlation_id=correlation_id,
    )


def _parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Probe FoxyLink HTTP endpoint and write diagnostic report."
    )
    parser.add_argument("--exchange", help="Exchange description used in URL.")
    parser.add_argument("--operation", help="Operation description used in URL.")
    parser.add_argument("--message-type", help="SYNC or ASYNC.")
    parser.add_argument("--foxy-path", help="HTTP service path, default /hs/AppEndpoint/.")
    parser.add_argument("--payload-file", help="Path to JSON payload file.")
    parser.add_argument("--reply-to", help="Optional REPLYTO header.")
    parser.add_argument("--app-id", help="Optional APPID header.")
    parser.add_argument("--correlation-id", help="Optional CORRELATIONID header.")
    parser.add_argument(
        "--output",
        default=str(DEFAULT_REPORT_PATH),
        help="Output report path (relative to project root or absolute).",
    )
    parser.add_argument(
        "--strict",
        action="store_true",
        help="Return non-zero code when endpoint is not reachable (HTTP 200).",
    )
    return parser.parse_args()


def main() -> int:
    args = _parse_args()
    settings = _load_settings(args)

    payload_obj, body, payload_source = _load_payload(
        args.payload_file, settings.payload_json
    )

    headers: dict[str, str] = {
        "Accept": "application/json",
        "Content-Type": "application/json; charset=utf-8",
    }
    if settings.reply_to:
        headers["REPLYTO"] = settings.reply_to
    if settings.app_id:
        headers["APPID"] = settings.app_id
    if settings.correlation_id:
        headers["CORRELATIONID"] = settings.correlation_id

    response_status_code: int | None = None
    response_headers: dict[str, str] = {}
    response_text = ""
    error_message: str | None = None
    elapsed_ms: int | None = None

    try:
        session = requests.Session()
        if settings.username:
            session.auth = (settings.username, settings.password)

        response = session.post(
            settings.endpoint_url,
            data=body.encode("utf-8"),
            headers=headers,
            timeout=settings.timeout,
            verify=settings.verify_tls,
        )
        response_status_code = response.status_code
        response_headers = dict(response.headers)
        response_text = response.text or ""
        elapsed_ms = int(response.elapsed.total_seconds() * 1000)
    except requests.RequestException as exc:
        error_message = f"{exc.__class__.__name__}: {exc}"

    classification = _classify(response_status_code, error_message)
    success = classification == "reachable"

    report = {
        "generated_at": utc_now_iso(),
        "request": {
            "url": settings.endpoint_url,
            "method": "POST",
            "timeout_sec": settings.timeout,
            "verify_tls": settings.verify_tls,
            "exchange": settings.exchange,
            "operation": settings.operation,
            "message_type": settings.message_type.upper(),
            "headers": headers,
            "payload_source": payload_source,
            "payload_preview": _preview(body),
            "payload_is_json": isinstance(payload_obj, (dict, list, int, float, bool, type(None))),
        },
        "response": {
            "status_code": response_status_code,
            "elapsed_ms": elapsed_ms,
            "headers": response_headers,
            "body_preview": _preview(response_text),
            "body_length": len(response_text),
        },
        "classification": classification,
        "success": success,
        "error": error_message,
        "notes": {
            "reachable_requires_http_200": True,
            "diagnostic_hint": (
                "HTTP 404 usually means FoxyLink HTTP service is not merged into the "
                "published infobase or not published with HTTP services."
            ),
        },
    }

    output_path = _resolve_path(args.output)
    output_path.parent.mkdir(parents=True, exist_ok=True)
    output_path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")

    print(f"[ok] saved: {output_path}")
    print(
        "[ok] foxylink_probe: "
        f"classification={classification}, "
        f"status={response_status_code}, "
        f"url={settings.endpoint_url}"
    )

    if args.strict and not success:
        return 2
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
