import json
import logging
import os
import re
from datetime import datetime
from typing import Any

from .context import get_request_id, get_trace_id, get_account_id, get_job_id


class PIIRedactionFilter(logging.Filter):
    """Redacts PII and secrets before logs are emitted.

    Handles: msg, args, exc_text, and common extra fields.
    """

    PATTERNS = [
        (
            re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"),
            "***@***",
        ),
        (re.compile(r"\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b"), "****-****-****-****"),
        (
            re.compile(r"Bearer\s+eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+"),
            "Bearer [REDACTED]",
        ),
        (re.compile(r"sk_live_[A-Za-z0-9]+"), "sk_live_[REDACTED]"),
        (re.compile(r"sk_test_[A-Za-z0-9]+"), "sk_test_[REDACTED]"),
        (re.compile(r"xoxb-[A-Za-z0-9-]+"), "xoxb-[REDACTED]"),
        (
            re.compile(r'"(api_key|apiKey|secret|password|token|authorization)":\s*"[^"]*"'),
            r'"\1": "[REDACTED]"',
        ),
    ]

    SENSITIVE_EXTRAS = {"headers", "payload", "body", "cookies", "authorization"}
    _exc_formatter = logging.Formatter()

    def filter(self, record: logging.LogRecord) -> bool:
        record.msg = self._redact(str(record.msg))

        if record.args:
            record.args = self._redact_args(record.args)

        if record.exc_info and not record.exc_text:
            record.exc_text = self._exc_formatter.formatException(record.exc_info)
        if record.exc_text:
            record.exc_text = self._redact(record.exc_text)

        for field in self.SENSITIVE_EXTRAS:
            if hasattr(record, field):
                value = getattr(record, field)
                if value is not None:
                    setattr(record, field, self._redact(str(value)))

        return True

    def _redact(self, text: str) -> str:
        for pattern, replacement in self.PATTERNS:
            text = pattern.sub(replacement, text)
        return text

    def _redact_args(self, args: Any) -> Any:
        if isinstance(args, dict):
            return {
                k: self._redact(str(v)) if isinstance(v, str) else v
                for k, v in args.items()
            }
        if isinstance(args, (list, tuple)):
            redacted = [self._redact(str(a)) if isinstance(a, str) else a for a in args]
            return tuple(redacted) if isinstance(args, tuple) else redacted
        return self._redact(str(args))


class ContextInjectingFilter(logging.Filter):
    """Injects request context from contextvars into LogRecord."""

    def filter(self, record: logging.LogRecord) -> bool:
        request_id = get_request_id()
        trace_id = get_trace_id()
        account_id = get_account_id()
        job_id = get_job_id()

        # Always set attributes (even if None) so formatters can use defaults
        record.request_id = request_id or ""
        record.trace_id = trace_id or ""
        record.account_id = account_id or ""
        record.job_id = job_id or ""

        return True


class GCPJSONFormatter(logging.Formatter):
    """Formats logs as JSON for Cloud Logging.

    Outputs correlation IDs both in ``logging.googleapis.com/labels`` (GCP
    convention) and as top-level fields for easier ad-hoc filtering in
    Cloud Logging (``jsonPayload.account_id = "xxx"``).

    When a ``trace_id`` is present (from ``X-Cloud-Trace-Context``), emits
    ``logging.googleapis.com/trace`` in the fully-qualified format that
    Cloud Logging uses to link log entries to Cloud Trace spans.
    """

    SEVERITY_MAP = {
        "DEBUG": "DEBUG",
        "INFO": "INFO",
        "WARNING": "WARNING",
        "ERROR": "ERROR",
        "CRITICAL": "CRITICAL",
    }

    _GCP_PROJECT = os.environ.get("GOOGLE_CLOUD_PROJECT") or os.environ.get(
        "GCP_PROJECT", ""
    )

    def format(self, record: logging.LogRecord) -> str:
        log_entry = {
            "severity": self.SEVERITY_MAP.get(record.levelname, "DEFAULT"),
            "message": record.getMessage(),
            "timestamp": datetime.utcnow().isoformat() + "Z",
            "logging.googleapis.com/labels": {
                "logger": record.name,
                "module": record.module,
            },
        }

        labels = log_entry["logging.googleapis.com/labels"]

        # request_id — always a UUID we control
        request_id = getattr(record, "request_id", "") or ""
        if request_id:
            labels["request_id"] = request_id
            log_entry["request_id"] = request_id

        # trace_id — from X-Cloud-Trace-Context, used for Cloud Trace linking
        trace_id = getattr(record, "trace_id", "") or ""
        if trace_id:
            labels["trace_id"] = trace_id
            log_entry["trace_id"] = trace_id
            if self._GCP_PROJECT:
                log_entry["logging.googleapis.com/trace"] = (
                    f"projects/{self._GCP_PROJECT}/traces/{trace_id}"
                )

        # account_id — tenant identifier
        account_id = getattr(record, "account_id", "") or ""
        if account_id:
            labels["account_id"] = account_id
            log_entry["account_id"] = account_id

        # job_id — worker job identifier
        job_id = getattr(record, "job_id", "") or ""
        if job_id:
            labels["job_id"] = job_id
            log_entry["job_id"] = job_id

        if record.exc_info:
            log_entry["stack_trace"] = record.exc_text or self.formatException(
                record.exc_info
            )

        return json.dumps(log_entry)
