from __future__ import annotations

import json
import os
import shutil
import subprocess
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional

from core.database import EventDatabase
from core.paths import application_root, config_path, database_path, runtime_root
from core.process_enrichment import ProcessEnricher
from core.rules import RuleEngine, validate_detection_config
from core.service_wrapper import RelayServiceWrapper
from core.threat import calculate_threat_score
from core.transaction import TransactionStore
from recovery.center import RecoveryCenter


PROJECT_ROOT = application_root()
CONFIG_PATH = config_path()
DATABASE_PATH = database_path()
TRANSACTION_PATH = runtime_root() / "remediation_transactions.db"
RECOVERY_PATH = runtime_root() / "recovery"
SEVERITY_ORDER = {"info": 0, "low": 1, "medium": 2, "high": 3, "critical": 4}


def format_timestamp(value: Any) -> str:
    if not value:
        return "—"
    text = str(value)
    try:
        parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
        return parsed.astimezone().strftime("%Y-%m-%d %H:%M:%S")
    except ValueError:
        return text


def metadata_summary(value: Any, limit: int = 90) -> str:
    if not value:
        return ""
    if isinstance(value, str):
        try:
            value = json.loads(value)
        except json.JSONDecodeError:
            pass
    text = json.dumps(value, sort_keys=True) if isinstance(value, (dict, list)) else str(value)
    return text if len(text) <= limit else text[: limit - 1] + "…"


def filter_rows(rows: Iterable[Dict[str, Any]], search: str = "", **filters: str) -> List[Dict[str, Any]]:
    needle = search.strip().lower()
    result = []
    for row in rows:
        if needle and needle not in " ".join(str(value).lower() for value in row.values()):
            continue
        if any(selected and selected != "All" and str(row.get(key, "")).lower() != selected.lower()
               for key, selected in filters.items()):
            continue
        result.append(row)
    return result


def validate_config(payload: Any) -> List[str]:
    if not isinstance(payload, dict):
        return ["Configuration root must be a JSON object."]
    errors: List[str] = []
    if not isinstance(payload.get("Enabled", True), bool):
        errors.append("Enabled must be true or false.")
    telemetry = payload.get("Telemetry")
    if not isinstance(telemetry, dict):
        errors.append("Telemetry must be an object.")
        return errors
    for key, minimum in (("PollIntervalSeconds", 1), ("HeartbeatIntervalSeconds", 5), ("RetentionDays", 1)):
        value = telemetry.get(key)
        if not isinstance(value, int) or isinstance(value, bool) or value < minimum:
            errors.append(f"Telemetry.{key} must be an integer of at least {minimum}.")
    if not isinstance(telemetry.get("Path"), str) or not telemetry.get("Path", "").strip():
        errors.append("Telemetry.Path must be a non-empty string.")
    for key in ("AllowList", "WatchList"):
        value = payload.get(key)
        if not isinstance(value, list) or any(not isinstance(item, str) or not item.strip() for item in value):
            errors.append(f"{key} must be a list of non-empty strings.")
    errors.extend(error for error in validate_detection_config(payload) if error not in errors)
    return errors


class ConfigurationAdapter:
    def __init__(self, path: str | Path = CONFIG_PATH):
        self.path = Path(path)

    def load(self) -> Dict[str, Any]:
        payload = json.loads(self.path.read_text(encoding="utf-8"))
        errors = validate_config(payload)
        if errors:
            raise ValueError(" ".join(errors))
        return payload

    def save(self, payload: Dict[str, Any]) -> Path:
        errors = validate_config(payload)
        if errors:
            raise ValueError(" ".join(errors))
        self.path.parent.mkdir(parents=True, exist_ok=True)
        stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
        backup = self.path.with_name(f"{self.path.name}.backup-{stamp}")
        if self.path.exists():
            shutil.copy2(self.path, backup)
        serialized = json.dumps(payload, indent=2, sort_keys=False) + "\n"
        handle, temporary_name = tempfile.mkstemp(prefix=self.path.name + ".", suffix=".tmp", dir=self.path.parent)
        try:
            with os.fdopen(handle, "w", encoding="utf-8") as stream:
                stream.write(serialized)
            os.replace(temporary_name, self.path)
        except Exception:
            Path(temporary_name).unlink(missing_ok=True)
            raise
        return backup


class RelayAdapter:
    def __init__(self, config_path: str | Path = CONFIG_PATH, wrapper: Optional[RelayServiceWrapper] = None):
        self.wrapper = wrapper or RelayServiceWrapper(config_path)

    def status(self) -> Dict[str, Any]:
        return self.wrapper.status()

    def start(self) -> Dict[str, Any]:
        return self.wrapper.start()

    def stop(self) -> Dict[str, Any]:
        return self.wrapper.stop()


class DriverStatusAdapter:
    def __init__(self, service_name: str = "ProcessMonitor"):
        self.service_name = service_name

    def status(self) -> Dict[str, Any]:
        result = {"service_name": self.service_name, "installed": False, "running": False,
                  "state": "Not installed", "binary_path": "—", "start_type": "—", "error": None}
        if os.name != "nt":
            result["error"] = "Driver status is available on Windows only."
            return result
        flags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
        try:
            query = subprocess.run(["sc.exe", "query", self.service_name], capture_output=True, text=True,
                                   creationflags=flags, timeout=5)
            text = (query.stdout + query.stderr).strip()
            if query.returncode != 0:
                result["error"] = text or "Service query failed."
                return result
            result["installed"] = True
            for line in text.splitlines():
                if "STATE" in line and ":" in line:
                    state = line.split(":", 1)[1].strip().split(None, 1)[-1]
                    result["state"] = state.title()
                    result["running"] = "RUNNING" in line.upper()
            config = subprocess.run(["sc.exe", "qc", self.service_name], capture_output=True, text=True,
                                    creationflags=flags, timeout=5)
            for line in config.stdout.splitlines():
                if "BINARY_PATH_NAME" in line:
                    result["binary_path"] = line.split(":", 1)[1].strip()
                elif "START_TYPE" in line:
                    result["start_type"] = line.split(":", 1)[1].strip()
        except (OSError, subprocess.SubprocessError) as exc:
            result["error"] = str(exc)
        return result


def health_from_status(driver: Dict[str, Any], relay: Dict[str, Any], jsonl_ok: bool, database_ok: bool) -> str:
    if driver.get("error") and driver.get("installed") or relay.get("error"):
        return "Error"
    if driver.get("running") and relay.get("running") and jsonl_ok and database_ok:
        return "Healthy"
    return "Degraded"


class DataAdapter:
    def __init__(self, db_path: str | Path = DATABASE_PATH, config_path: str | Path = CONFIG_PATH):
        self.database = EventDatabase(db_path)
        self.config_path = Path(config_path)
        self.rule_engine = RuleEngine(self.config_path)
        self.process_enricher = ProcessEnricher()

    def recent_events(self, limit: int = 250) -> List[Dict[str, Any]]:
        return self.database.list_events(limit)

    def alerts(self, limit: int = 250) -> List[Dict[str, Any]]:
        return self.database.list_alerts(limit)

    def active_processes(self, limit: int = 1000) -> List[Dict[str, Any]]:
        active: Dict[int, Dict[str, Any]] = {}
        for event in reversed(self.database.list_events(limit)):
            pid = event.get("pid")
            if pid is None:
                continue
            kind = str(event.get("event_type") or "")
            if kind in {"process_started", "process_create", "process_snapshot"}:
                active[int(pid)] = event
            elif kind in {"process_exited", "process_exit"}:
                active.pop(int(pid), None)
        classified = []
        for event in active.values():
            item = dict(event)
            metadata = self._metadata(item.get("metadata"))
            for key in ("parent_process_name", "signature_status", "signature_message", "first_seen", "last_seen", "matched_rules"):
                if key in metadata:
                    item[key] = metadata[key]
            item["sha256"] = item.get("hash_value")
            decision = self.rule_engine.classify(item)
            item.update({key: decision[key] for key in ("classification", "classification_reason", "severity", "risk_score", "rule_id", "matched_rules")})
            item.setdefault("first_seen", item.get("timestamp"))
            item["last_seen"] = item.get("last_seen") or item.get("timestamp")
            classified.append(item)
        return classified

    @staticmethod
    def _metadata(value: Any) -> Dict[str, Any]:
        if isinstance(value, dict):
            return value
        if isinstance(value, str):
            try:
                payload = json.loads(value)
                return payload if isinstance(payload, dict) else {}
            except json.JSONDecodeError:
                return {}
        return {}

    def process_details(self, process: Dict[str, Any]) -> Dict[str, Any]:
        details = dict(process)
        if not details.get("sha256") and details.get("path"):
            details["sha256"] = self.process_enricher.sha256(details["path"])
        signed = details.get("signed")
        details["signature_status"] = details.get("signature_status") or (
            "Valid" if signed is True else "NotSigned" if signed is False else "Unknown"
        )
        return details

    def alert_details(self, alert: Dict[str, Any]) -> Dict[str, Any]:
        details = dict(alert)
        metadata = self._metadata(details.get("metadata"))
        for key in ("classification", "classification_reason", "matched_rules", "parent_process_name",
                    "signature_status", "first_seen", "last_seen"):
            if key in metadata:
                details[key] = metadata[key]
        details.setdefault("classification", "UNKNOWN")
        details.setdefault("matched_rules", [])
        return details

    def threat_score(self) -> Dict[str, Any]:
        return calculate_threat_score(self.alerts(1000))

    def metrics(self) -> Dict[str, Any]:
        result = self.database.get_dashboard_metrics()
        result["active_process_count"] = len(self.active_processes())
        return result

    def jsonl_path(self) -> Path:
        try:
            return Path(json.loads(self.config_path.read_text(encoding="utf-8"))["Telemetry"]["Path"])
        except (OSError, KeyError, TypeError, json.JSONDecodeError):
            return Path("C:/Logs/process_monitor_events.jsonl")

    def jsonl_events(self, limit: int = 500) -> List[Dict[str, Any]]:
        path = self.jsonl_path()
        if not path.exists():
            return []
        lines = path.read_text(encoding="utf-8", errors="replace").splitlines()[-limit:]
        events = []
        for line in reversed(lines):
            try:
                item = json.loads(line)
                if isinstance(item, dict):
                    events.append(item)
            except json.JSONDecodeError:
                continue
        return events


class RemediationReadAdapter:
    def __init__(self, transaction_path: str | Path = TRANSACTION_PATH, recovery_path: str | Path = RECOVERY_PATH,
                 event_db_path: str | Path = DATABASE_PATH):
        self.transaction_path = Path(transaction_path)
        self.recovery_path = Path(recovery_path)
        self.event_db_path = Path(event_db_path)

    def snapshot(self) -> Dict[str, Any]:
        transactions = TransactionStore(self.transaction_path).list_transactions(100) if self.transaction_path.exists() else []
        restore_file = self.recovery_path / "restore_items.json"
        if restore_file.exists():
            recovery = RecoveryCenter(self.recovery_path)
            recovery_items = recovery.list_items()
            recovery_summary = recovery.summary()
        else:
            recovery_items = []
            recovery_summary = {"total_items": 0, "restorable_count": 0, "needs_review": 0}
        audit = EventDatabase(self.event_db_path).list_audit_entries(100) if self.event_db_path.exists() else []
        return {"transactions": transactions, "recovery": recovery_items,
                "recovery_summary": recovery_summary, "audit": audit}
