"""Logger structurat central.

Singurul punct prin care se emit evenimente de aplicatie: garanteaza format,
redactare si dublul canal (app_events in DB + log text rotativ) consistente si
imposibil de ocolit. Best-effort: o cadere a jurnalului NU doboara cererea/worker-ul.

Redactare la SCRIERE (nu la afisare): toate valorile trec prin `redact_pii`
(creds/token mascate integral, VIN/nr partial) inainte de persistare.
"""

from __future__ import annotations

import contextvars
import json
import logging
import logging.handlers
from datetime import datetime, timedelta, timezone
from typing import Any

from .config import get_settings
from .db import get_connection, insert_app_event
from .security import redact_pii, scrub_text

# request_id al cererii curente. Setat de middleware-ul HTTP; disponibil in
# handlerul de erori si aici, fara a polua semnaturile de functii.
request_id_var: contextvars.ContextVar[str | None] = contextvars.ContextVar(
    "request_id", default=None
)

_LEVELS = {"DEBUG": 10, "INFO": 20, "WARNING": 30, "WARN": 30, "ERROR": 40, "CRITICAL": 50}

# Sursa implicita a evenimentelor pentru procesul curent. API = 'api' (default);
# worker-ul cheama set_source('worker') la pornire (fisier per-proces).
_DEFAULT_SOURCE = "api"

_loggers: dict[str, logging.Logger] = {}


def set_source(sursa: str) -> None:
    """Fixeaza sursa implicita a evenimentelor (apelata o data de worker la start)."""
    global _DEFAULT_SOURCE
    _DEFAULT_SOURCE = sursa


def _text_logger(sursa: str) -> logging.Logger:
    """Logger cu RotatingFileHandler pe fisier per-proces (app-<sursa>.log).

    Rotatia pe dimensiune e in aplicatie — nu depindem de deploy. Cheia de cache
    include calea: la schimbarea log_dir (teste) se creeaza un logger nou, fara a
    acumula handlere duplicate pe acelasi fisier.
    """
    settings = get_settings()
    path = settings.log_dir / f"app-{sursa}.log"
    key = str(path)
    lg = _loggers.get(key)
    if lg is not None:
        return lg
    lg = logging.getLogger(f"autopass.events::{key}")
    lg.setLevel(logging.DEBUG)
    lg.propagate = False
    try:
        settings.log_dir.mkdir(parents=True, exist_ok=True)
        handler = logging.handlers.RotatingFileHandler(
            path,
            maxBytes=settings.log_file_max_bytes,
            backupCount=settings.log_file_backup_count,
            encoding="utf-8",
        )
        handler.setFormatter(logging.Formatter("%(asctime)s %(message)s"))
        lg.addHandler(handler)
    except Exception:  # noqa: BLE001 — fisier indisponibil nu trebuie sa doboare logul DB
        pass
    _loggers[key] = lg
    return lg


def _purge_after(days: int) -> str:
    """now (UTC) + days, in formatul SQLite datetime('now') ('YYYY-MM-DD HH:MM:SS')."""
    return (datetime.now(timezone.utc) + timedelta(days=days)).strftime("%Y-%m-%d %H:%M:%S")


def log_event(
    tip: str,
    *,
    nivel: str = "INFO",
    account_id: int | None = None,
    cod: str | None = None,
    mesaj: str | None = None,
    context: dict | None = None,
    sursa: str | None = None,
    request_id: str | None = None,
    conn: Any = None,
) -> None:
    """Emite un eveniment: un rand `app_events` + o linie in logul text (acelasi continut redactat).

    - `tip`: text liber documentat (lista extensibila).
    - `nivel`: DEBUG|INFO|WARNING|ERROR|CRITICAL. Sub `AUTOPASS_LOG_LEVEL` -> ignorat.
    - `context`: metadate (submission_id, count, status...) — NU payload PII integral.
    - `conn`: reutilizeaza conexiunea apelantului pe calea fierbinte (evita contentie WAL);
      None -> deschide/inchide o conexiune proprie.
    Best-effort: orice exceptie e inghitita (jurnalul nu trebuie sa rupa fluxul).
    """
    try:
        settings = get_settings()
        min_lvl = _LEVELS.get((settings.log_level or "INFO").upper(), 20)
        lvl = (nivel or "INFO").upper()
        if _LEVELS.get(lvl, 20) < min_lvl:
            return

        src = sursa or _DEFAULT_SOURCE
        rid = request_id if request_id is not None else request_id_var.get()
        mesaj_red = scrub_text(mesaj) if isinstance(mesaj, str) else mesaj
        ctx_red = redact_pii(context) if context else None
        ctx_json = (
            json.dumps(ctx_red, ensure_ascii=False, default=str) if ctx_red is not None else None
        )
        purge_after = _purge_after(int(settings.log_retention_days))

        own = conn is None
        c = conn or get_connection()
        try:
            insert_app_event(
                c,
                request_id=rid,
                account_id=account_id,
                sursa=src,
                tip=tip,
                nivel=lvl,
                cod=cod,
                mesaj=mesaj_red,
                context_json=ctx_json,
                purge_after=purge_after,
            )
        finally:
            if own:
                c.close()

        line = (
            f"[{src}] tip={tip} nivel={lvl} cont={account_id} cod={cod} "
            f"rid={rid} {mesaj_red or ''}"
        )
        if ctx_json:
            line += f" ctx={ctx_json}"
        _text_logger(src).info(scrub_text(line))
    except Exception:  # noqa: BLE001 — jurnal best-effort (ca notify_signup)
        pass
