from __future__ import annotations

import hashlib
import json
import os
import subprocess
from pathlib import Path
from typing import Any, Callable, Dict, Optional, Tuple


class ProcessEnricher:
    """Efficient, best-effort Windows process metadata enrichment."""

    def __init__(self, runner: Optional[Callable[..., Any]] = None):
        self._runner = runner or subprocess.run
        self._signature_cache: Dict[Tuple[str, Optional[int], Optional[int]], Dict[str, Any]] = {}
        self._hash_cache: Dict[Tuple[str, Optional[int], Optional[int]], Optional[str]] = {}

    @staticmethod
    def empty_process(pid: int, name: str = "Unknown") -> Dict[str, Any]:
        return {
            "pid": pid, "process_name": name or "Unknown", "path": None, "parent_pid": None,
            "parent_process_name": None, "command_line": None, "creation_time": None,
            "signer": None, "signed": None, "signature_status": "Unknown", "sha256": None,
        }

    def snapshot(self) -> Dict[int, Dict[str, Any]]:
        if os.name != "nt":
            return {}
        script = (
            "Get-CimInstance Win32_Process | Select-Object ProcessId,Name,ExecutablePath,"
            "ParentProcessId,CommandLine,@{Name='CreationTime';Expression={if($_.CreationDate){"
            "$_.CreationDate.ToUniversalTime().ToString('o')}}} | ConvertTo-Json -Compress"
        )
        try:
            completed = self._runner(
                ["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", script],
                capture_output=True, text=True, check=True, timeout=15,
                creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
            )
            payload = json.loads(completed.stdout or "[]")
            rows = payload if isinstance(payload, list) else [payload]
        except (OSError, subprocess.SubprocessError, json.JSONDecodeError, TypeError):
            return {}

        result: Dict[int, Dict[str, Any]] = {}
        for row in rows:
            if not isinstance(row, dict):
                continue
            try:
                pid = int(row.get("ProcessId"))
            except (TypeError, ValueError):
                continue
            name = str(row.get("Name") or "Unknown")
            command_line = row.get("CommandLine") or None
            if name.lower() in {"powershell.exe", "pwsh.exe"} and command_line and "Get-CimInstance Win32_Process" in command_line:
                continue  # Exclude only this collector's short-lived query helper.
            item = self.empty_process(pid, name)
            try:
                parent_pid = int(row.get("ParentProcessId"))
            except (TypeError, ValueError):
                parent_pid = None
            item.update({
                "path": row.get("ExecutablePath") or None,
                "parent_pid": parent_pid,
                "command_line": command_line,
                "creation_time": row.get("CreationTime") or row.get("CreationDate") or None,
            })
            result[pid] = item
        for item in result.values():
            parent = result.get(item.get("parent_pid"))
            item["parent_process_name"] = parent.get("process_name") if parent else None
        return result

    def enrich_signature(self, process: Dict[str, Any]) -> Dict[str, Any]:
        enriched = dict(process)
        path = enriched.get("path")
        if not path:
            return enriched
        key = self._file_identity(path)
        if key not in self._signature_cache:
            self._signature_cache[key] = self._query_signature(str(path))
        enriched.update(self._signature_cache[key])
        return enriched

    def sha256(self, path: Optional[str]) -> Optional[str]:
        if not path:
            return None
        key = self._file_identity(path)
        if key not in self._hash_cache:
            try:
                digest = hashlib.sha256()
                with Path(path).open("rb") as handle:
                    for chunk in iter(lambda: handle.read(1024 * 1024), b""):
                        digest.update(chunk)
                self._hash_cache[key] = digest.hexdigest()
            except OSError:
                self._hash_cache[key] = None
        return self._hash_cache[key]

    @staticmethod
    def _file_identity(path: str) -> Tuple[str, Optional[int], Optional[int]]:
        normalized = os.path.normcase(os.path.abspath(path))
        try:
            stat = Path(path).stat()
            return normalized, stat.st_size, stat.st_mtime_ns
        except OSError:
            return normalized, None, None

    def _query_signature(self, path: str) -> Dict[str, Any]:
        script = (
            "$s=Get-AuthenticodeSignature -LiteralPath $env:PROCESSMONITOR_SIGNATURE_PATH; "
            "[pscustomobject]@{Status=[string]$s.Status;StatusMessage=$s.StatusMessage;"
            "Signer=if($s.SignerCertificate){$s.SignerCertificate.Subject}else{$null}} | ConvertTo-Json -Compress"
        )
        try:
            environment = os.environ.copy()
            environment["PROCESSMONITOR_SIGNATURE_PATH"] = path
            completed = self._runner(
                ["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", script],
                capture_output=True, text=True, check=True, timeout=10,
                env=environment,
                creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
            )
            payload = json.loads(completed.stdout or "{}")
            status = str(payload.get("Status") or "Unknown")
            signed = True if status == "Valid" else False if status == "NotSigned" else None
            return {"signer": payload.get("Signer") or None, "signed": signed,
                    "signature_status": status, "signature_message": payload.get("StatusMessage") or None}
        except (OSError, subprocess.SubprocessError, json.JSONDecodeError, TypeError):
            return {"signer": None, "signed": None, "signature_status": "Unavailable",
                    "signature_message": "Signature information unavailable"}
