from __future__ import annotations

import json
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional

from core.paths import config_path as default_config_path

SEVERITY_RISK = {"info": 0, "low": 10, "medium": 25, "high": 70, "critical": 90}
SEVERITY_ORDER = {name: index for index, name in enumerate(SEVERITY_RISK)}
CORRELATION_BONUS_PER_CATEGORY = 10
MAX_CORRELATION_BONUS = 20


@dataclass
class DetectionRule:
    """Legacy generic rule retained for callers of evaluate()."""
    rule_id: str
    name: str
    description: str
    severity: str = "medium"
    enabled: bool = True
    conditions: Dict[str, Any] = field(default_factory=dict)

    def matches(self, event: Dict[str, Any]) -> bool:
        if not self.enabled:
            return False
        name = str(event.get("process_name") or "").lower()
        command = str(event.get("command_line") or "").lower()
        tags = {str(tag).lower() for tag in event.get("tags") or []}
        return bool(
            self.conditions.get("process_name") and str(self.conditions["process_name"]).lower() in name
            or self.conditions.get("command_line_keyword") and str(self.conditions["command_line_keyword"]).lower() in command
            or self.conditions.get("tag") and str(self.conditions["tag"]).lower() in tags
        )


def _valid_rule(rule: Any, required: tuple[str, ...]) -> bool:
    return bool(isinstance(rule, dict)
                and all(isinstance(rule.get(key), str) and rule[key].strip() for key in required)
                and str(rule.get("Severity", "medium")).lower() in SEVERITY_RISK
                and ("RiskContribution" not in rule or isinstance(rule["RiskContribution"], int)
                     and not isinstance(rule["RiskContribution"], bool) and 0 <= rule["RiskContribution"] <= 100)
                and ("Enabled" not in rule or isinstance(rule["Enabled"], bool)))


def validate_detection_config(payload: Any) -> List[str]:
    """Validate structured detection policy. Invalid entries never become rules."""
    if not isinstance(payload, dict):
        return ["Configuration root must be a JSON object."]
    rules = payload.get("AlertRules", {})
    if not isinstance(rules, dict):
        return ["AlertRules must be an object."]
    errors: List[str] = []
    keywords = rules.get("HighRiskKeywords", [])
    if not isinstance(keywords, list) or any(not isinstance(v, str) or not v.strip() for v in keywords):
        errors.append("AlertRules.HighRiskKeywords must be a list of non-empty strings.")
    for key, required in (("CommandLineIndicators", ("RuleId", "Indicator")),
                          ("ParentChildRules", ("RuleId", "Parent", "Child"))):
        configured = rules.get(key, [])
        if not isinstance(configured, list):
            errors.append(f"AlertRules.{key} must be a list.")
            continue
        for index, rule in enumerate(configured):
            if not _valid_rule(rule, required):
                fields = ", ".join(required)
                errors.append(f"AlertRules.{key}[{index}] must contain non-empty {fields} strings with a valid Severity.")
    return errors


class RuleEngine:
    """Configuration-driven, explainable process rule engine."""
    def __init__(self, config_path: Optional[str | Path] = None):
        self.config_path = Path(config_path) if config_path is not None else default_config_path()
        self.config = self._load_config()
        self.rules = [
            DetectionRule("WATCH-001", "Watchlisted process", "Process matches a known watchlist entry",
                          "medium", conditions={"process_name": "ollama.exe"}),
            DetectionRule("WATCH-002", "Suspicious command line", "Command line includes configured execution keywords",
                          "high", conditions={"command_line_keyword": "powershell"}),
            DetectionRule("WATCH-003", "Python shell execution", "Python runtime invoked in a one-liner execution pattern",
                          "high", conditions={"command_line_keyword": "python -c"}),
        ]

    def _load_config(self) -> Dict[str, Any]:
        try:
            payload = json.loads(self.config_path.read_text(encoding="utf-8"))
        except (FileNotFoundError, json.JSONDecodeError, OSError):
            return {}
        return payload if isinstance(payload, dict) and not validate_detection_config(payload) else {}

    def classify(self, event: Dict[str, Any]) -> Dict[str, Any]:
        process_name = str(event.get("process_name") or event.get("image") or "").strip()
        if not process_name:
            return self._decision("UNKNOWN", "Insufficient process context", "info", 0, [])
        findings = self._command_findings(event) + self._parent_child_findings(event)
        watchlist = {str(v).strip().lower() for v in self.config.get("WatchList", [])}
        if process_name.lower() in watchlist:
            findings.append(self._finding("CFG-WATCHLIST", "watchlist_process", "medium", 25,
                                         "Configured watchlist match", {"process_name": process_name}, event))
        findings = self._deduplicate(findings)
        if findings:
            strongest = max(findings, key=lambda f: (SEVERITY_ORDER[f["severity"]], f["risk_contribution"], f["rule_id"]))
            classification = "HIGH_RISK" if strongest["severity"] in {"high", "critical"} else "WATCHLISTED"
            return self._decision(classification, strongest["reason"], strongest["severity"],
                                  self.correlated_risk(findings), findings)
        allowlist = {str(v).strip().lower() for v in self.config.get("AllowList", [])}
        if process_name.lower() in allowlist:
            return self._decision("TRUSTED", "Explicit allowlist match", "info", 0, [])
        return self._decision("NORMAL", "No detection rules matched", "info", 0, [])

    def _command_findings(self, event: Dict[str, Any]) -> List[Dict[str, Any]]:
        command = event.get("command_line")
        if not isinstance(command, str) or not command.strip():
            return []
        configured = self.config.get("AlertRules", {})
        rules = list(configured.get("CommandLineIndicators", []))
        rules += [{"RuleId": f"CFG-HIGH-{i:03d}", "Indicator": value, "Severity": "high", "RiskContribution": 70}
                  for i, value in enumerate(configured.get("HighRiskKeywords", []), 1)]
        findings = []
        for rule in rules:
            if rule.get("Enabled", True) and str(rule["Indicator"]).lower() in command.lower():
                severity = str(rule.get("Severity", "high")).lower()
                findings.append(self._finding(str(rule["RuleId"]), "command_line", severity,
                    int(rule.get("RiskContribution", SEVERITY_RISK[severity])),
                    str(rule.get("Reason") or ("High-risk command-line keyword matched" if str(rule["RuleId"]).startswith("CFG-HIGH-") else "Configured command-line indicator matched")),
                    {"indicator": str(rule["Indicator"]), "command_line": command}, event))
        return findings

    def _parent_child_findings(self, event: Dict[str, Any]) -> List[Dict[str, Any]]:
        parent = event.get("parent_process_name")
        child = event.get("process_name") or event.get("image")
        if not isinstance(parent, str) or not parent.strip() or not isinstance(child, str) or not child.strip():
            return []
        findings = []
        for rule in self.config.get("AlertRules", {}).get("ParentChildRules", []):
            if (rule.get("Enabled", True) and parent.lower() == str(rule["Parent"]).strip().lower()
                    and child.lower() == str(rule["Child"]).strip().lower()):
                severity = str(rule.get("Severity", "medium")).lower()
                findings.append(self._finding(str(rule["RuleId"]), "parent_child", severity,
                    int(rule.get("RiskContribution", SEVERITY_RISK[severity])),
                    str(rule.get("Reason") or "Configured parent/child relationship matched"),
                    {"parent_process": parent, "child_process": child}, event))
        return findings

    @staticmethod
    def _finding(rule_id: str, category: str, severity: str, risk: int, reason: str,
                 evidence: Dict[str, Any], event: Dict[str, Any]) -> Dict[str, Any]:
        return {"rule_id": rule_id, "category": category, "severity": severity,
                "risk_contribution": max(0, min(100, risk)), "reason": reason, "evidence": evidence,
                "timestamp": event.get("timestamp") or datetime.now(timezone.utc).isoformat(timespec="milliseconds")}

    @staticmethod
    def _deduplicate(findings: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        unique: Dict[tuple[str, str], Dict[str, Any]] = {}
        for finding in findings:
            unique.setdefault((finding["rule_id"].lower(), finding["category"].lower()), finding)
        return list(unique.values())

    @staticmethod
    def correlated_risk(findings: List[Dict[str, Any]]) -> int:
        """Strongest contribution + 10 per extra independent category; bonus capped at 20."""
        unique = RuleEngine._deduplicate(findings)
        if not unique:
            return 0
        base = max(int(f.get("risk_contribution") or 0) for f in unique)
        categories = {str(f.get("category") or "").lower() for f in unique if f.get("category")}
        bonus = min(MAX_CORRELATION_BONUS, max(0, len(categories) - 1) * CORRELATION_BONUS_PER_CATEGORY)
        return min(100, base + bonus)

    @staticmethod
    def _decision(classification: str, reason: str, severity: str, risk_score: int,
                  findings: List[Dict[str, Any]]) -> Dict[str, Any]:
        strongest = max(findings, key=lambda f: (SEVERITY_ORDER[f["severity"]], f["risk_contribution"], f["rule_id"]), default=None)
        return {"classification": classification, "classification_reason": reason, "severity": severity,
                "risk_score": risk_score, "alert": bool(findings),
                "rule_id": strongest["rule_id"] if strongest else None, "reason": reason,
                "matched_rules": findings}

    def build_alert(self, event: Dict[str, Any], decision: Dict[str, Any]) -> Optional[Dict[str, Any]]:
        if not decision.get("alert"):
            return None
        return {"alert_id": f"{decision['rule_id']}-{event.get('event_id') or uuid.uuid4().hex}",
                "timestamp": event.get("timestamp"), "severity": decision["severity"],
                "title": f"{decision['classification'].replace('_', ' ').title()} process finding",
                "description": decision["reason"], "rule_id": decision["rule_id"], "status": "New",
                "process_id": event.get("pid"), "event_id": event.get("event_id"), "analyst_notes": "",
                "process": event.get("process_name") or event.get("image"), "pid": event.get("pid"),
                "reason": decision["reason"], "risk_score": decision["risk_score"],
                "classification": decision["classification"], "matched_rules": decision["matched_rules"],
                "source": event.get("source", "relay_service")}

    def load_from_file(self) -> List[DetectionRule]:
        return self.rules

    def evaluate(self, event: Dict[str, Any]) -> List[Dict[str, Any]]:
        return [{"alert_id": f"{rule.rule_id}-{event.get('event_id', 'unknown')}", "rule_id": rule.rule_id,
                 "title": rule.name, "description": rule.description, "severity": rule.severity,
                 "status": "new", "event_id": event.get("event_id")}
                for rule in self.rules if rule.matches(event)]
