"""Debug log analysis engine – pattern matching, severity classification,
governor-limit parsing, and false-positive exclusion."""

import re
from dataclasses import dataclass, field

# ── severity levels ──────────────────────────────────────────────────────

CRITICAL = "CRITICAL"
ERROR = "ERROR"
WARNING = "WARNING"
INFO = "INFO"

# ── pattern definitions ──────────────────────────────────────────────────
# Each tuple: (compiled regex, severity, human-readable label)

_RAW_PATTERNS: list[tuple[str, str, str]] = [
    # CRITICAL
    (r"FATAL_ERROR", CRITICAL, "Fatal Error"),
    (r"System\.LimitException", CRITICAL, "Governor Limit Exception"),
    (r"UNABLE_TO_LOCK_ROW", CRITICAL, "Row Lock Failure"),
    (r"TOO_MANY_SOQL_QUERIES", CRITICAL, "SOQL Limit Exceeded"),
    (r"TOO_MANY_DML_STATEMENTS", CRITICAL, "DML Limit Exceeded"),
    (r"System\.AsyncException", CRITICAL, "Async Exception"),
    # ERROR – exceptions
    (r"EXCEPTION_THROWN", ERROR, "Exception Thrown"),
    (r"System\.DmlException", ERROR, "DML Exception"),
    (r"System\.NullPointerException", ERROR, "Null Pointer Exception"),
    (r"System\.QueryException", ERROR, "Query Exception"),
    (r"System\.CalloutException", ERROR, "Callout Exception"),
    (r"System\.TypeException", ERROR, "Type Exception"),
    (r"System\.ListException", ERROR, "List Exception"),
    (r"System\.StringException", ERROR, "String Exception"),
    (r"System\.JSONException", ERROR, "JSON Exception"),
    (r"System\.SecurityException", ERROR, "Security Exception"),
    (r"System\.NoAccessException", ERROR, "No Access Exception"),
    (r"System\.SObjectException", ERROR, "SObject Exception"),
    (r"System\.MathException", ERROR, "Math Exception"),
    (r"System\.AssertException", ERROR, "Assert Exception"),
    (r"System\.UnexpectedException", ERROR, "Unexpected Exception"),
    # ERROR – DML outcomes
    (r"INSERT_FAILED", ERROR, "Insert Failed"),
    (r"UPDATE_FAILED", ERROR, "Update Failed"),
    (r"DELETE_FAILED", ERROR, "Delete Failed"),
    (r"UPSERT_FAILED", ERROR, "Upsert Failed"),
    (r"FIELD_CUSTOM_VALIDATION_EXCEPTION", ERROR, "Validation Rule Failure"),
    (r"REQUIRED_FIELD_MISSING", ERROR, "Required Field Missing"),
    (r"DUPLICATE_VALUE", ERROR, "Duplicate Value"),
    (r"INVALID_CROSS_REFERENCE_KEY", ERROR, "Invalid Cross-Reference Key"),
    (r"CANNOT_INSERT_UPDATE_ACTIVATE", ERROR, "Cannot Insert/Update/Activate"),
    (r"Unhandled trigger exception", ERROR, "Unhandled Trigger Exception"),
    (r"threw unhandled exception", ERROR, "Unhandled Exception"),
    (r"caused by:", ERROR, "Caused-By Chain"),
    # WARNING
    (r"System\.HandledException", WARNING, "Handled Exception"),
    (r"VF_PAGE_MESSAGE", WARNING, "Visualforce Page Message"),
    (r"FLOW_ELEMENT_FAULT", WARNING, "Flow Element Fault"),
    (r"FLOW_ELEMENT_ERROR", WARNING, "Flow Element Error"),
]

PATTERNS = [(re.compile(p), sev, label) for p, sev, label in _RAW_PATTERNS]

# ── false-positive exclusions ────────────────────────────────────────────
# Lines matching any of these are silently skipped.

_FALSE_POSITIVE_STRINGS = [
    "__sfdc_ERROR",
    "__sfdc_FAILED",
    "VARIABLE_SCOPE_BEGIN",
    "VARIABLE_ASSIGNMENT",
    "HEAP_ALLOCATE",
    "LIMIT_USAGE_FOR_NS",
    "CUMULATIVE_LIMIT_USAGE",
    "CUMULATIVE_LIMIT_USAGE_END",
    "TOTAL_EMAIL_RECIPIENTS_QUEUED",
]

# ── governor limit thresholds (fraction at which we warn) ────────────────

_GOV_LIMIT_THRESHOLD = 0.80

_GOV_LIMIT_RE = re.compile(
    r"Number of (\w[\w\s]+\w):\s+(\d+)\s+out of\s+(\d+)"
)
_CPU_LIMIT_RE = re.compile(
    r"Maximum CPU time:\s+(\d+)\s+out of\s+(\d+)"
)
_HEAP_LIMIT_RE = re.compile(
    r"Maximum heap size:\s+(\d+)\s+out of\s+(\d+)"
)


# ── data classes ─────────────────────────────────────────────────────────

@dataclass
class Finding:
    line: int
    severity: str
    label: str
    content: str


@dataclass
class GovernorUsage:
    name: str
    used: int
    limit: int

    @property
    def pct(self) -> float:
        return (self.used / self.limit * 100) if self.limit else 0.0

    @property
    def is_warning(self) -> bool:
        return self.pct >= _GOV_LIMIT_THRESHOLD * 100


@dataclass
class ScanResult:
    log_id: str
    findings: list[Finding] = field(default_factory=list)
    governor_usage: list[GovernorUsage] = field(default_factory=list)
    namespaces: list[str] = field(default_factory=list)

    @property
    def has_issues(self) -> bool:
        return bool(self.findings) or any(g.is_warning for g in self.governor_usage)

    @property
    def max_severity(self) -> str:
        order = {CRITICAL: 0, ERROR: 1, WARNING: 2, INFO: 3}
        if not self.findings:
            if any(g.is_warning for g in self.governor_usage):
                return WARNING
            return INFO
        return min(self.findings, key=lambda f: order.get(f.severity, 99)).severity

    def to_dict(self) -> dict:
        return {
            "log_id": self.log_id,
            "has_issues": self.has_issues,
            "max_severity": self.max_severity,
            "findings": [
                {
                    "line": f.line,
                    "severity": f.severity,
                    "label": f.label,
                    "content": f.content,
                }
                for f in self.findings
            ],
            "governor_usage": [
                {
                    "name": g.name,
                    "used": g.used,
                    "limit": g.limit,
                    "pct": round(g.pct, 1),
                    "is_warning": g.is_warning,
                }
                for g in self.governor_usage
            ],
            "namespaces": self.namespaces,
        }


# ── namespace extraction ─────────────────────────────────────────────────

_NS_RE = re.compile(r"\b([A-Z][A-Za-z0-9]{2,15})\.\w+\.\w+")


def _extract_namespaces(log_text: str) -> list[str]:
    found: set[str] = set()
    for m in _NS_RE.finditer(log_text[:200_000]):
        ns = m.group(1)
        if ns not in ("System", "Database", "Schema", "Messaging", "Auth",
                       "UserInfo", "Limits", "JSON", "String", "Integer",
                       "Boolean", "Double", "Date", "Datetime", "Decimal",
                       "List", "Map", "Set", "Type", "Http", "Url",
                       "ApexPages", "Test", "Trigger", "ConnectApi"):
            found.add(ns)
    return sorted(found)


# ── main scanner ─────────────────────────────────────────────────────────

def scan_log(log_id: str, log_text: str) -> ScanResult:
    """Scan a single debug log body and return categorised findings."""
    result = ScanResult(log_id=log_id)
    seen_keys: set[str] = set()

    for line_num, raw_line in enumerate(log_text.split("\n"), 1):
        line = raw_line.strip()
        if not line:
            continue

        if any(fp in line for fp in _FALSE_POSITIVE_STRINGS):
            continue

        # governor limits
        for rx in (_GOV_LIMIT_RE, _CPU_LIMIT_RE, _HEAP_LIMIT_RE):
            m = rx.search(line)
            if m:
                groups = m.groups()
                if len(groups) == 3:
                    name, used, limit = groups
                elif len(groups) == 2:
                    name = "CPU time" if rx is _CPU_LIMIT_RE else "Heap size"
                    used, limit = groups
                else:
                    continue
                gu = GovernorUsage(name=name.strip(), used=int(used), limit=int(limit))
                if gu.is_warning:
                    result.governor_usage.append(gu)

        # error patterns
        for pattern, severity, label in PATTERNS:
            if pattern.search(line):
                key = f"{label}:{line[:120]}"
                if key not in seen_keys:
                    seen_keys.add(key)
                    result.findings.append(
                        Finding(
                            line=line_num,
                            severity=severity,
                            label=label,
                            content=line[:300],
                        )
                    )
                break

    result.namespaces = _extract_namespaces(log_text)
    return result


def search_logs(log_text: str, query: str) -> list[dict]:
    """Return lines that match a free-text query (case-insensitive)."""
    results = []
    tokens = query.lower().split()
    for num, raw in enumerate(log_text.split("\n"), 1):
        lower = raw.lower()
        if all(t in lower for t in tokens):
            results.append({"line": num, "content": raw.strip()[:300]})
        if len(results) >= 500:
            break
    return results
