from __future__ import annotations

"""
Externes Logging-Modul (MySQL / PostgreSQL) auf Basis von SQLAlchemy
====================================================================

Zweck
- Persistentes Logging in eine externe Datenbank (MySQL oder PostgreSQL).
- Optionale Fallback-Strategie auf internes SQLite-Logging bei Fehlern.

Features
- Connection-Pooling via SQLAlchemy Engine.
- Schema-Erzeugung (Tabelle logs) via SQLAlchemy Core.
- Batch-Schreiben von Logeinträgen.
- Health-Check (SELECT 1).
- logging.Handler zum direkten Schreiben aus Python-Logging.
- Optionaler Fallback (Schreiben in internes SQLite-Logging), wenn aktiviert.

Konfiguration (Beispiel; siehe 01_Modulerweiterungen/Planung/Architektur.md)
logging_external:
  enabled: false
  type: "postgresql"  # mysql | postgresql
  host: "localhost"
  port: 5432
  user: "logger"
  password: null
  database: "logs"
  sslmode: "prefer"
  pool_size: 5
  connect_timeout: 10
  write_buffer_size: 100
  fallback_to_internal_on_error: true
"""

import json
import logging
import time
import os
import threading
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union

try:
    from sqlalchemy import (
        create_engine,
        Table,
        Column,
        Integer,
        String,
        Text,
        MetaData,
        DateTime,
        text as sa_text,
        insert as sa_insert,
    )
    from sqlalchemy.engine import Engine, Connection
except Exception as e:  # pragma: no cover
    raise RuntimeError(
        "SQLAlchemy ist erforderlich für logging_external. Bitte Abhängigkeiten installieren."
    ) from e


def _env_override(key: str, default: Optional[str] = None) -> Optional[str]:
    """Liest optionale Secrets aus Umgebungsvariablen, falls gesetzt."""
    v = os.environ.get(key)
    return v if v is not None else default


def _safe_quote(v: str) -> str:
    # Nur einfache, naive Maskierung für URL-Bestandteile
    return v.replace("@", "%40").replace(":", "%3A").replace("/", "%2F")


def _now_utc_iso() -> str:
    return (
        datetime.now(timezone.utc)
        .astimezone(timezone.utc)
        .replace(tzinfo=timezone.utc)
        .isoformat(timespec="milliseconds")
        .replace("+00:00", "Z")
    )


@dataclass
class ExternalConfig:
    db_type: str = "postgresql"  # "mysql" | "postgresql"
    host: str = "localhost"
    port: int = 5432
    user: Optional[str] = None
    password: Optional[str] = None
    database: str = "logs"
    sslmode: Optional[str] = None  # nur relevant für PostgreSQL
    pool_size: int = 5
    connect_timeout: int = 10
    fallback_to_internal_on_error: bool = True

    @classmethod
    def from_dict(cls, cfg: Dict[str, Any]) -> "ExternalConfig":
        # Secrets via ENV überschreiben, wenn vorhanden
        user = cfg.get("user")
        password = cfg.get("password")
        user = _env_override("LOG_EXT_USER", user)
        password = _env_override("LOG_EXT_PASSWORD", password)

        return cls(
            db_type=str(cfg.get("type", "postgresql")),
            host=str(cfg.get("host", "localhost")),
            port=int(cfg.get("port", 5432)),
            user=(None if user is None else str(user)),
            password=(None if password is None else str(password)),
            database=str(cfg.get("database", "logs")),
            sslmode=(cfg.get("sslmode") if cfg.get("sslmode") is not None else None),
            pool_size=int(cfg.get("pool_size", 5)),
            connect_timeout=int(cfg.get("connect_timeout", 10)),
            fallback_to_internal_on_error=bool(cfg.get("fallback_to_internal_on_error", True)),
        )

    def to_url(self) -> str:
        """
        Baut den SQLAlchemy-URL-String auf Basis der Konfiguration.
        - PostgreSQL: postgresql+psycopg2://user:password@host:port/database?sslmode=...
        - MySQL:      mysql+pymysql://user:password@host:port/database
        """
        user_part = ""
        if self.user:
            if self.password:
                user_part = f"{_safe_quote(self.user)}:{_safe_quote(self.password)}@"
            else:
                user_part = f"{_safe_quote(self.user)}@"

        if self.db_type.lower() in ("postgres", "postgresql", "pg"):
            base = f"postgresql+psycopg2://{user_part}{self.host}:{self.port}/{self.database}"
            if self.sslmode:
                base = f"{base}?sslmode={self.sslmode}"
            return base

        if self.db_type.lower() in ("mysql", "mariadb"):
            return f"mysql+pymysql://{user_part}{self.host}:{self.port}/{self.database}"

        raise ValueError(f"Unsupported db_type: {self.db_type}")


class ExternalDBLogger:
    """
    Verwaltet Engine und Schema für externes Logging.
    """

    def __init__(self, engine: Engine) -> None:
        self.engine = engine
        self.meta = MetaData()
        # Hinweis: ts als String/DateTime. Für maximale Kompatibilität nutzen wir String(30) ISO-Zeitstempel.
        self.logs = Table(
            "logs",
            self.meta,
            Column("id", Integer, primary_key=True, autoincrement=True),
            Column("ts", String(30), nullable=False),
            Column("level", String(16), nullable=True),
            Column("logger", String(128), nullable=True),
            Column("message", Text, nullable=True),
            Column("meta", Text, nullable=True),  # JSON-String
        )

    # ---------- API ----------

    def ensure_schema(self) -> None:
        self.meta.create_all(self.engine, checkfirst=True)

    def health_check(self, timeout_s: int = 5) -> bool:
        try:
            with self.engine.connect() as conn:
                conn.execute(sa_text("SELECT 1"))
            return True
        except Exception:
            return False

    def write_batch(self, entries: Iterable[Dict[str, Any]]) -> None:
        rows: List[Dict[str, Any]] = []
        for e in entries:
            ts = e.get("ts") or _now_utc_iso()
            level = e.get("level")
            loggername = e.get("logger")
            message = e.get("message")
            meta = e.get("meta")
            try:
                meta_json = json.dumps(meta, ensure_ascii=False, separators=(",", ":")) if meta is not None else None
            except Exception:
                meta_json = json.dumps({"_repr": repr(meta)}, ensure_ascii=False)
            rows.append(
                {
                    "ts": str(ts),
                    "level": (None if level is None else str(level)),
                    "logger": (None if loggername is None else str(loggername)),
                    "message": (None if message is None else str(message)),
                    "meta": meta_json,
                }
            )
        if not rows:
            return

        with self.engine.begin() as conn:
            conn.execute(sa_insert(self.logs), rows)


class ExternalDBHandler(logging.Handler):
    """
    Logging-Handler, der LogRecords in die externe DB schreibt.
    Mit einfachem Retry/Backoff und optionalem Fallback auf internes SQLite.
    """

    def __init__(self, writer: ExternalDBLogger, *, fallback_to_internal: bool = True) -> None:
        super().__init__()
        self._writer = writer
        self._fallback_to_internal = fallback_to_internal

    def emit(self, record: logging.LogRecord) -> None:
        payload = {
            "ts": _now_utc_iso(),
            "level": record.levelname,
            "logger": record.name,
            "message": self.format(record) if self.formatter else record.getMessage(),
            "meta": _extract_meta(record),
        }
        # Kleiner Retry mit Backoff: 3 Versuche 50ms/150ms
        delay_ms = [0.05, 0.15, 0.5]
        last_err: Optional[BaseException] = None
        for d in delay_ms:
            try:
                self._writer.write_batch([payload])
                return
            except Exception as e:
                last_err = e
                time.sleep(d)
        # Fallback
        if self._fallback_to_internal:
            try:
                import importlib
                mod = importlib.import_module("logging_internal")
                mod.instance().write(payload)  # nutzt bestehende interne Instanz
                logging.getLogger(__name__).warning(
                    "External logging failed; wrote to internal SQLite fallback."
                )
                return
            except Exception:
                pass
        # Letzter Ausweg: Fehler im Handler nicht nach außen eskalieren
        if last_err:
            self.handleError(record)


def _extract_meta(record: logging.LogRecord) -> Dict[str, Any]:
    meta: Dict[str, Any] = {
        "pathname": record.pathname,
        "lineno": record.lineno,
        "funcName": record.funcName,
        "process": record.process,
        "threadName": record.threadName,
    }
    standard = {
        "name",
        "msg",
        "args",
        "levelname",
        "levelno",
        "pathname",
        "filename",
        "module",
        "exc_info",
        "exc_text",
        "stack_info",
        "lineno",
        "funcName",
        "created",
        "msecs",
        "relativeCreated",
        "thread",
        "threadName",
        "processName",
        "process",
        "message",
    }
    for k, v in record.__dict__.items():
        if k not in standard:
            try:
                json.dumps(v)
                meta[k] = v
            except Exception:
                meta[k] = repr(v)
    if record.exc_info:
        try:
            meta["exc_info"] = logging.Formatter().formatException(record.exc_info)
        except Exception:
            pass
    return meta


# ---------------------------------------------------------
# Modul-Singleton und Helper
# ---------------------------------------------------------

_EXTERNAL_INSTANCE: Optional[ExternalDBLogger] = None
_INSTANCE_LOCK = threading.Lock()


def init(
    *,
    connection_url: Optional[str] = None,
    config: Optional[Dict[str, Any]] = None,
) -> ExternalDBLogger:
    """
    Initialisiert den globalen ExternalDBLogger. Entweder via connection_url oder via config.
    Mehrfache Aufrufe liefern dieselbe Instanz zurück.
    """
    global _EXTERNAL_INSTANCE
    with _INSTANCE_LOCK:
        if _EXTERNAL_INSTANCE is not None:
            return _EXTERNAL_INSTANCE

        if connection_url is None:
            if not isinstance(config, dict):
                raise ValueError("Entweder connection_url angeben oder config (Dict) bereitstellen.")
            cfg = ExternalConfig.from_dict(config)
            connection_url = cfg.to_url()
            pool_size = cfg.pool_size
            connect_timeout = cfg.connect_timeout
        else:
            # Fallback-Parameter wenn URL direkt übergeben wird
            pool_size = int((config or {}).get("pool_size", 5))
            connect_timeout = int((config or {}).get("connect_timeout", 10))
            cfg = ExternalConfig.from_dict(config or {})

        engine = create_engine(
            connection_url,
            pool_pre_ping=True,
            pool_size=pool_size,
            connect_args={},  # sslmode ist bei PG bereits in der URL verarbeitet
            echo=False,
        )
        logger = ExternalDBLogger(engine)
        logger.ensure_schema()
        _EXTERNAL_INSTANCE = logger
        # Speichere Fallback-Flag am Logger-Objekt (attributiv), damit Handler darauf zugreifen kann, wenn benötigt
        setattr(_EXTERNAL_INSTANCE, "_fallback_to_internal_on_error", cfg.fallback_to_internal_on_error)
        return _EXTERNAL_INSTANCE


def instance() -> ExternalDBLogger:
    if _EXTERNAL_INSTANCE is None:
        raise RuntimeError("ExternalDBLogger ist nicht initialisiert. Bitte init(...) zuerst aufrufen.")
    return _EXTERNAL_INSTANCE


def get_handler(level: Union[int, str] = logging.INFO) -> logging.Handler:
    if _EXTERNAL_INSTANCE is None:
        raise RuntimeError("ExternalDBLogger ist nicht initialisiert. Bitte init(...) zuerst aufrufen.")
    h = ExternalDBHandler(
        _EXTERNAL_INSTANCE,
        fallback_to_internal=bool(getattr(_EXTERNAL_INSTANCE, "_fallback_to_internal_on_error", True)),
    )
    if isinstance(level, str):
        level = getattr(logging, level.upper(), logging.INFO)
    h.setLevel(int(level))
    return h


__all__ = [
    "ExternalConfig",
    "ExternalDBLogger",
    "init",
    "instance",
    "get_handler",
]