"""Debug logging for Hikaflow CLI sessions.

Enable with HIKAFLOW_DEBUG=1 or force-enable via init_debug_log(force=True)
to write structured JSONL logs to ~/.hikaflow/debug.log and per-session files
under ~/.hikaflow/debug/sessions/YYYY-MM-DD/<session_id>.jsonl.

Usage:
    from autodebug.debug_log import init_debug_log, log_event

    init_debug_log(force=True)  # call once at session start
    log_event("tool.call", tool_name="scan_codebase", args_preview="...")
"""

from __future__ import annotations

import itertools
import json
import logging
import os
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Optional


_debug_logger: logging.Logger | None = None
_session_id: Optional[str] = None
_session_log_path: Optional[Path] = None
_session_dir: Optional[Path] = None
_seq_counter: itertools.count = itertools.count(1)
_http_patched: bool = False
_session_start_ts: Optional[str] = None
_MAX_SERVER_LOG_BYTES: int = 10 * 1024 * 1024  # 10 MB cap for fetched server logs


def is_debug_enabled() -> bool:
    """Check if debug logging is enabled via HIKAFLOW_DEBUG env var."""
    return os.getenv("HIKAFLOW_DEBUG", "").strip().lower() in ("1", "true", "yes")


def get_log_path() -> Path:
    """Get the debug log file path."""
    return Path.home() / ".hikaflow" / "debug.log"

def _utc_now() -> str:
    return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")


def _truncate(value: str, max_len: int) -> str:
    if len(value) <= max_len:
        return value
    if max_len < 12:
        return value[:max_len]
    return value[: max_len - 12] + f"...(len={len(value)})"


_PII_PATTERNS = [
    (r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b", "[EMAIL]"),
    (r"(?:Bearer\s+|sk-|pk_|rk_|whsec_)[A-Za-z0-9_\-]{20,}", "[TOKEN]"),
    (r"\b\d{3}-\d{2}-\d{4}\b", "[SSN]"),
    (r"\b(?:\d{4}[- ]?){3}\d{4}\b", "[CARD]"),
]


def _scrub_pii(value: str) -> str:
    import re

    for pattern, replacement in _PII_PATTERNS:
        value = re.sub(pattern, replacement, value)
    return value


_SENSITIVE_KEY_PATTERNS = frozenset({
    "password", "secret", "token", "api_key", "apikey",
    "access_key", "private_key", "credential", "auth",
})


def _scrub_payload(value: Any, _depth: int = 0, _max_depth: int = 20, _max_breadth: int = 200) -> Any:
    if _depth >= _max_depth:
        return "[TRUNCATED:max_depth]"
    if isinstance(value, str):
        return _scrub_pii(value)
    if isinstance(value, dict):
        items = list(value.items())
        if len(items) > _max_breadth:
            items = items[:_max_breadth]
            truncated = {k: _scrub_payload(v, _depth + 1, _max_depth, _max_breadth) for k, v in items}
            truncated["__truncated__"] = f"[TRUNCATED:{len(value)} keys, showed {_max_breadth}]"
            return {
                k: ("[REDACTED]" if any(s in k.lower() for s in _SENSITIVE_KEY_PATTERNS) else v)
                for k, v in truncated.items()
            }
        return {
            k: (
                "[REDACTED]" if any(s in k.lower() for s in _SENSITIVE_KEY_PATTERNS)
                else _scrub_payload(v, _depth + 1, _max_depth, _max_breadth)
            )
            for k, v in items
        }
    if isinstance(value, list):
        if len(value) > _max_breadth:
            return [_scrub_payload(v, _depth + 1, _max_depth, _max_breadth) for v in value[:_max_breadth]] + [f"[TRUNCATED:{len(value)} items]"]
        return [_scrub_payload(v, _depth + 1, _max_depth, _max_breadth) for v in value]
    return value


class _JSONLFormatter(logging.Formatter):
    def format(self, record: logging.LogRecord) -> str:
        payload = getattr(record, "payload", None)
        if payload is None:
            payload = {"message": record.getMessage()}
        if "timestamp" not in payload:
            payload["timestamp"] = _utc_now()
        return json.dumps(payload, ensure_ascii=True, default=str)


def _default_max_field() -> int:
    try:
        return int(os.getenv("HIKAFLOW_DEBUG_MAX_FIELD", "20000"))
    except Exception:
        return 20000


def _new_session_id() -> str:
    return str(uuid.uuid4())


def _session_log_dir(base: Path) -> Path:
    today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
    return base / "debug" / "sessions" / today


def init_debug_log(
    session_id: Optional[str] = None,
    context: Optional[Dict[str, Any]] = None,
    force: bool = False,
) -> Optional[str]:
    """Initialize structured debug logging.

    Writes to:
      - ~/.hikaflow/debug.log (combined log, append)
      - ~/.hikaflow/debug/sessions/YYYY-MM-DD/<session_id>.jsonl (per-session)
    """
    global _debug_logger, _session_id, _session_log_path, _session_dir, _seq_counter, _session_start_ts
    if not (force or is_debug_enabled()):
        return None

    base_dir = Path.home() / ".hikaflow"
    base_dir.mkdir(parents=True, exist_ok=True)

    _session_id = session_id or _new_session_id()
    _session_start_ts = _utc_now()
    _session_dir = _session_log_dir(base_dir)
    _session_dir.mkdir(parents=True, exist_ok=True)
    _session_log_path = _session_dir / f"{_session_id}.jsonl"

    _debug_logger = logging.getLogger("hikaflow.debug")
    _debug_logger.setLevel(logging.DEBUG)
    _debug_logger.handlers.clear()

    combined_handler = logging.FileHandler(get_log_path(), mode="a", encoding="utf-8")
    combined_handler.setFormatter(_JSONLFormatter())

    session_handler = logging.FileHandler(_session_log_path, mode="a", encoding="utf-8")
    session_handler.setFormatter(_JSONLFormatter())

    _debug_logger.addHandler(combined_handler)
    _debug_logger.addHandler(session_handler)
    _debug_logger.propagate = False  # don't pollute terminal

    _seq_counter = itertools.count(1)

    log_event(
        "session.start",
        session_id=_session_id,
        session_log_path=str(_session_log_path),
        context=context or {},
        debug_enabled=True,
    )
    _install_http_logging()
    return _session_id


def get_session_log_path() -> Optional[Path]:
    return _session_log_path


def log_event(event: str, **fields: Any) -> None:
    """Write a structured debug log entry."""
    if not _debug_logger:
        return
    seq = next(_seq_counter)
    payload: Dict[str, Any] = {
        "timestamp": _utc_now(),
        "event": event,
        "seq": seq,
    }
    if _session_id:
        payload["session_id"] = _session_id
    max_len = _default_max_field()
    for key, value in fields.items():
        if isinstance(value, str):
            payload[key] = _truncate(value, max_len)
        else:
            payload[key] = value
    if os.getenv("HIKAFLOW_DEBUG_SCRUB", "1").strip().lower() in ("1", "true", "yes"):
        payload = _scrub_payload(payload)
    _debug_logger.debug("", extra={"payload": payload})


def write_session_artifact(kind: str, content: str, ext: str = "txt") -> Optional[str]:
    """Write large log payloads to a session file and return its path."""
    if not _session_dir or not _session_id:
        return None
    safe_kind = "".join(c if c.isalnum() or c in ("-", "_") else "_" for c in kind)
    # Sanitize ext to prevent path traversal (reject /, .., and control chars)
    ext = "".join(c if c.isalnum() or c in ("-", "_", ".") else "" for c in ext)
    if not ext or ".." in ext:
        ext = "txt"
    ts = datetime.now(timezone.utc).strftime("%H%M%S")
    suffix = uuid.uuid4().hex[:6]
    path = _session_dir / f"{_session_id}.{safe_kind}.{ts}.{suffix}.{ext}"
    try:
        path.write_text(content, encoding="utf-8")
        return str(path)
    except Exception:
        return None


def debug(msg: str, **fields: Any) -> None:
    """Backward-compatible debug log."""
    log_event("debug.message", message=msg, **fields)


def finalize_debug_log(summary: Optional[Dict[str, Any]] = None) -> None:
    """Write a session end record and optional summary file."""
    if not _debug_logger or not _session_id or not _session_dir:
        return
    try:
        _fetch_server_logs()
    except Exception:
        pass  # Don't let server log fetch crash the finalization
    log_event("session.end", summary=summary or {})
    _uninstall_http_logging()
    if summary:
        try:
            summary_path = _session_dir / f"{_session_id}.summary.json"
            summary_path.write_text(json.dumps(summary, indent=2, sort_keys=True), encoding="utf-8")
        except Exception:
            pass


def _fetch_server_logs() -> None:
    """Optional fetch of server-side logs if configured."""
    if not _debug_logger or not _session_dir or not _session_id:
        return
    logs_url = os.getenv("HIKAFLOW_SERVER_LOGS_URL", "").strip()
    if not logs_url:
        log_event("server_logs.skip", reason="not_configured")
        return
    try:
        import requests

        headers = {}
        token = os.getenv("HIKAFLOW_SERVER_LOGS_TOKEN", "").strip()
        if not token:
            try:
                from autodebug.config import get_valid_token
                token = get_valid_token() or ""
            except Exception:
                token = ""
        if token:
            headers["Authorization"] = f"Bearer {token}"
        params = {"session_id": _session_id}
        if _session_start_ts:
            params["since"] = _session_start_ts

        resp = requests.get(logs_url, headers=headers, params=params, timeout=30, stream=True)
        if resp.status_code >= 400:
            # Read a bounded preview — resp.text would download the entire
            # body and defeat the stream=True size cap.
            error_chunks: list[bytes] = []
            error_total = 0
            for echunk in resp.iter_content(chunk_size=2048):
                error_total += len(echunk)
                error_chunks.append(echunk)
                if error_total >= 2000:
                    break
            body_preview = b"".join(error_chunks).decode("utf-8", errors="replace")[:2000]
            log_event(
                "server_logs.error",
                status=resp.status_code,
                body_preview=body_preview,
            )
            resp.close()
            return

        # Read with size cap to prevent filling disk
        chunks: list[bytes] = []
        total = 0
        for chunk in resp.iter_content(chunk_size=65536):
            total += len(chunk)
            if total > _MAX_SERVER_LOG_BYTES:
                log_event("server_logs.truncated", max_bytes=_MAX_SERVER_LOG_BYTES)
                break
            chunks.append(chunk)
        content = b"".join(chunks)

        server_log_path = _session_dir / f"{_session_id}.server.log"
        server_log_path.write_bytes(content)
        log_event(
            "server_logs.fetched",
            status=resp.status_code,
            size_bytes=len(content),
            path=str(server_log_path),
        )
    except Exception as e:
        log_event(
            "server_logs.error",
            error_type=type(e).__name__,
            error=str(e),
        )


def _filter_headers(headers: Dict[str, Any]) -> Dict[str, Any]:
    filtered = {}
    for k, v in headers.items():
        lk = k.lower()
        if lk in ("authorization", "cookie", "set-cookie", "x-api-key"):
            filtered[k] = "[REDACTED]"
        else:
            filtered[k] = v
    return filtered


_http_originals: Dict[str, Any] = {}


def _uninstall_http_logging() -> None:
    """Restore original HTTP methods, undoing the monkeypatch."""
    global _http_patched
    if not _http_patched:
        return
    try:
        import requests
        if "requests.Session.request" in _http_originals:
            requests.Session.request = _http_originals["requests.Session.request"]  # type: ignore[assignment]
    except Exception:
        pass
    try:
        import httpx
        if "httpx.Client.request" in _http_originals:
            httpx.Client.request = _http_originals["httpx.Client.request"]  # type: ignore[assignment]
        if "httpx.AsyncClient.request" in _http_originals:
            httpx.AsyncClient.request = _http_originals["httpx.AsyncClient.request"]  # type: ignore[assignment]
    except Exception:
        pass
    _http_originals.clear()
    _http_patched = False


def _install_http_logging() -> None:
    global _http_patched
    if _http_patched:
        return

    # requests (sync)
    try:
        import requests

        _orig_request = requests.Session.request
        _http_originals["requests.Session.request"] = _orig_request

        def _logged_request(self, method, url, **kwargs):
            start = time.monotonic()
            headers = dict(kwargs.get("headers") or {})  # copy to avoid mutating caller's dict
            if _session_id and "X-Hikaflow-Session-Id" not in headers:
                headers["X-Hikaflow-Session-Id"] = _session_id
            kwargs["headers"] = headers
            try:
                resp = _orig_request(self, method, url, **kwargs)
                duration_ms = int((time.monotonic() - start) * 1000)
                log_event(
                    "http.request",
                    method=method,
                    url=url,
                    status=resp.status_code,
                    duration_ms=duration_ms,
                    request_headers=_filter_headers(kwargs.get("headers") or {}),
                    response_headers=_filter_headers(dict(resp.headers or {})),
                    request_body_len=len(kwargs.get("data") or b"") if isinstance(kwargs.get("data"), (bytes, bytearray)) else None,
                    response_body_len=int(resp.headers.get("content-length", 0)) or None,
                    timeout=kwargs.get("timeout"),
                )
                return resp
            except Exception as e:
                duration_ms = int((time.monotonic() - start) * 1000)
                log_event(
                    "http.error",
                    method=method,
                    url=url,
                    duration_ms=duration_ms,
                    error_type=type(e).__name__,
                    error=str(e),
                )
                raise

        requests.Session.request = _logged_request  # type: ignore[assignment]
    except Exception:
        pass

    # httpx (sync + async)
    try:
        import httpx

        _orig_httpx_request = httpx.Client.request
        _http_originals["httpx.Client.request"] = _orig_httpx_request

        def _logged_httpx_request(self, method, url, **kwargs):
            start = time.monotonic()
            headers = kwargs.get("headers") or {}
            if _session_id and "X-Hikaflow-Session-Id" not in headers:
                headers["X-Hikaflow-Session-Id"] = _session_id
            kwargs["headers"] = headers
            try:
                resp = _orig_httpx_request(self, method, url, **kwargs)
                duration_ms = int((time.monotonic() - start) * 1000)
                log_event(
                    "http.request",
                    method=method,
                    url=str(url),
                    status=resp.status_code,
                    duration_ms=duration_ms,
                    request_headers=_filter_headers(kwargs.get("headers") or {}),
                    response_headers=_filter_headers(dict(resp.headers or {})),
                    timeout=kwargs.get("timeout"),
                )
                return resp
            except Exception as e:
                duration_ms = int((time.monotonic() - start) * 1000)
                log_event(
                    "http.error",
                    method=method,
                    url=str(url),
                    duration_ms=duration_ms,
                    error_type=type(e).__name__,
                    error=str(e),
                )
                raise

        httpx.Client.request = _logged_httpx_request  # type: ignore[assignment]

        _orig_httpx_async_request = httpx.AsyncClient.request
        _http_originals["httpx.AsyncClient.request"] = _orig_httpx_async_request

        async def _logged_httpx_async_request(self, method, url, **kwargs):
            start = time.monotonic()
            headers = kwargs.get("headers") or {}
            if _session_id and "X-Hikaflow-Session-Id" not in headers:
                headers["X-Hikaflow-Session-Id"] = _session_id
            kwargs["headers"] = headers
            try:
                resp = await _orig_httpx_async_request(self, method, url, **kwargs)
                duration_ms = int((time.monotonic() - start) * 1000)
                log_event(
                    "http.request",
                    method=method,
                    url=str(url),
                    status=resp.status_code,
                    duration_ms=duration_ms,
                    request_headers=_filter_headers(kwargs.get("headers") or {}),
                    response_headers=_filter_headers(dict(resp.headers or {})),
                    timeout=kwargs.get("timeout"),
                )
                return resp
            except Exception as e:
                duration_ms = int((time.monotonic() - start) * 1000)
                log_event(
                    "http.error",
                    method=method,
                    url=str(url),
                    duration_ms=duration_ms,
                    error_type=type(e).__name__,
                    error=str(e),
                )
                raise

        httpx.AsyncClient.request = _logged_httpx_async_request  # type: ignore[assignment]
    except Exception:
        pass

    # Set flag only after patching succeeds — if both blocks above raised,
    # _http_originals will be empty and _uninstall_http_logging is a no-op.
    if _http_originals:
        _http_patched = True


# NOTE: `debug()` is defined earlier with `**fields` support.
