from __future__ import annotations

import json
from datetime import datetime, timezone
from decimal import Decimal
from pathlib import Path
from typing import Any

from pydantic import SecretStr


REDACTED = "***REDACTED***"
SENSITIVE_KEY_PARTS = (
    "api_key",
    "apikey",
    "api-secret",
    "api_secret",
    "apisecret",
    "secret",
    "signature",
    "password",
    "token",
    "authorization",
    "x-mbx-apikey",
)


class AuditLogger:
    def __init__(
        self,
        log_dir: str | Path = "logs",
        *,
        filename: str = "audit.log",
        max_bytes: int = 20 * 1024 * 1024,
        backup_count: int = 5,
        max_entry_bytes: int = 256 * 1024,
    ) -> None:
        self.log_dir = Path(log_dir)
        self.filename = filename
        self.max_bytes = max_bytes
        self.backup_count = backup_count
        self.max_entry_bytes = max_entry_bytes

    def log(
        self,
        event: str,
        *,
        params: Any | None = None,
        formula: Any | None = None,
        result: Any | None = None,
        error: Any | None = None,
        level: str = "INFO",
    ) -> None:
        payload: dict[str, Any] = {
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "level": level,
            "event": event,
        }
        if params is not None:
            payload["params"] = _sanitize(params)
        if formula is not None:
            payload["formula"] = _sanitize(formula)
        if result is not None:
            payload["result"] = _sanitize(result)
        if error is not None:
            payload["error"] = _sanitize(error)

        entry = _format_entry(payload)
        entry = self._limit_entry_size(payload, entry)
        self.log_dir.mkdir(parents=True, exist_ok=True)
        log_path = self.log_dir / self.filename
        self._rotate_if_needed(log_path, entry)
        with log_path.open("a", encoding="utf-8", errors="replace") as handle:
            handle.write(entry)

    def _limit_entry_size(self, payload: dict[str, Any], entry: str) -> str:
        if self.max_entry_bytes <= 0:
            return entry
        if len(entry.encode("utf-8", errors="replace")) <= self.max_entry_bytes:
            return entry
        limited_payload = {
            "timestamp": payload["timestamp"],
            "level": payload["level"],
            "event": payload["event"],
            "result": {
                "truncated": True,
                "original_bytes": len(entry.encode("utf-8", errors="replace")),
                "max_entry_bytes": self.max_entry_bytes,
                "available_sections": sorted(key for key in payload if key not in {"timestamp", "level", "event"}),
            },
        }
        limited_entry = _format_entry(limited_payload)
        if len(limited_entry.encode("utf-8", errors="replace")) <= self.max_entry_bytes:
            return limited_entry
        json_payload = json.dumps(limited_payload, ensure_ascii=False, separators=(",", ":"))
        compact = (
            f"{'=' * 80}\n"
            f"Time: {payload['timestamp']}\n"
            f"Level: {payload['level']}\n"
            f"Event: {payload['event']}\n"
            f"Result: truncated; original entry exceeded max_entry_bytes={self.max_entry_bytes}\n"
            "JSON:\n"
            f"{json_payload}\n"
        )
        encoded = compact.encode("utf-8", errors="replace")
        if len(encoded) <= self.max_entry_bytes:
            return compact
        marker = "\n...[truncated]\n"
        keep_bytes = max(0, self.max_entry_bytes - len(marker.encode("utf-8")))
        return encoded[:keep_bytes].decode("utf-8", errors="ignore") + marker

    def _rotate_if_needed(self, log_path: Path, entry: str) -> None:
        if self.max_bytes <= 0 or self.backup_count <= 0:
            return
        entry_size = len(entry.encode("utf-8", errors="replace"))
        if not log_path.exists() or log_path.stat().st_size + entry_size <= self.max_bytes:
            return

        oldest = log_path.with_name(f"{log_path.name}.{self.backup_count}")
        if oldest.exists():
            oldest.unlink()
        for index in range(self.backup_count - 1, 0, -1):
            source = log_path.with_name(f"{log_path.name}.{index}")
            if source.exists():
                source.replace(log_path.with_name(f"{log_path.name}.{index + 1}"))
        log_path.replace(log_path.with_name(f"{log_path.name}.1"))


def audit_event(
    event: str,
    *,
    params: Any | None = None,
    formula: Any | None = None,
    result: Any | None = None,
    error: Any | None = None,
    level: str = "INFO",
    log_dir: str | Path = "logs",
) -> None:
    try:
        AuditLogger(log_dir=log_dir).log(
            event,
            params=params,
            formula=formula,
            result=result,
            error=error,
            level=level,
        )
    except Exception:
        pass


def _format_entry(payload: dict[str, Any]) -> str:
    lines = [
        "=" * 80,
        f"Time: {payload['timestamp']}",
        f"Level: {payload['level']}",
        f"Event: {payload['event']}",
    ]
    for label, key in (
        ("Params", "params"),
        ("Formula", "formula"),
        ("Result", "result"),
        ("Error", "error"),
    ):
        if key not in payload:
            continue
        lines.extend([f"{label}:", _pretty_json(payload[key])])
    lines.extend(
        [
            "JSON:",
            json.dumps(payload, ensure_ascii=False, separators=(",", ":")),
            "",
        ]
    )
    return "\n".join(lines)


def _pretty_json(value: Any) -> str:
    return json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2)


def _sanitize(value: Any, *, key: str | None = None) -> Any:
    if key is not None and _is_sensitive_key(key):
        return REDACTED
    if isinstance(value, SecretStr):
        return REDACTED
    if isinstance(value, Decimal):
        return format(value, "f")
    if isinstance(value, Path):
        return str(value)
    if isinstance(value, dict):
        return {str(item_key): _sanitize(item_value, key=str(item_key)) for item_key, item_value in value.items()}
    if isinstance(value, (list, tuple)):
        return [_sanitize(item) for item in value]
    if isinstance(value, set):
        return sorted(_sanitize(item) for item in value)
    if isinstance(value, Exception):
        return {"type": type(value).__name__, "message": str(value)}
    if isinstance(value, (str, int, float, bool)) or value is None:
        return value
    if hasattr(value, "model_dump"):
        return _sanitize(value.model_dump())
    if hasattr(value, "__dict__"):
        return _sanitize(vars(value))
    return str(value)


def _is_sensitive_key(key: str) -> bool:
    normalized = key.lower().replace("-", "_")
    return any(part in normalized for part in SENSITIVE_KEY_PARTS)
