"""Filesystem detector helpers.

Signals generated here come from traversing files and evaluating heuristics/IOCs.
"""

from __future__ import annotations

import hashlib
import os
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Callable, Optional

from ..reason_codes import (
    ENV_SCANNER_PARTIAL_RESULTS,
    HEUR_NEW_EXEC_IN_SENSITIVE_PATH,
    HEUR_RECENT_SUSPICIOUS_EXTENSION,
    HEUR_UNEXPECTED_MODIFICATION_NEAR_WALLET,
    IOC_HASH_KNOWN_MALICIOUS,
)


@dataclass(slots=True)
class FilesystemDetectorResult:
    files_scanned: int
    files_hashed: int
    partial: bool


def _file_sha256(path: Path) -> Optional[str]:
    h = hashlib.sha256()
    try:
        with path.open("rb") as fh:
            for chunk in iter(lambda: fh.read(8192), b""):
                h.update(chunk)
        return h.hexdigest()
    except OSError:
        return None


def _safe_stat(path: Path) -> Optional[tuple[int, float]]:
    try:
        st = path.stat()
        return int(st.st_size), float(st.st_mtime)
    except OSError:
        return None


def _path_has_token(path: str, tokens: set[str]) -> bool:
    lower = path.lower()
    return any(token in lower for token in tokens)


def run_filesystem_detectors(
    *,
    scan_path: Path,
    started_at: float,
    max_scan_seconds: int,
    max_files: int,
    recent_cutoff: float,
    suspicious_extensions: set[str],
    wallet_tokens: set[str],
    baseline: dict[str, dict],
    known_bad_hashes: set[str],
    add_signal: Callable[[str, float, dict], None],
) -> FilesystemDetectorResult:
    """Traverse filesystem and emit detector signals via callback."""
    files_scanned = 0
    files_hashed = 0
    partial = False

    for root, dirs, files in os.walk(scan_path):
        if (time.time() - started_at) > max_scan_seconds or files_scanned >= max_files:
            partial = True
            break

        for name in files:
            if (time.time() - started_at) > max_scan_seconds or files_scanned >= max_files:
                partial = True
                break

            full = Path(root) / name
            st = _safe_stat(full)
            if not st:
                continue

            size, mtime = st
            files_scanned += 1
            path_str = str(full)
            ext = full.suffix.lower()
            in_sensitive_path = _path_has_token(path_str, wallet_tokens)
            baseline_record = baseline.get(path_str)
            is_new = baseline_record is None
            is_modified = False
            if baseline_record:
                is_modified = (
                    baseline_record.get("size") != size
                    or baseline_record.get("mtime") != mtime
                )

            if mtime >= recent_cutoff and ext in suspicious_extensions:
                add_signal(
                    HEUR_RECENT_SUSPICIOUS_EXTENSION,
                    0.7,
                    {"path": path_str, "ext": ext, "mtime": mtime, "size": size},
                )

            if is_new and ext in suspicious_extensions and in_sensitive_path:
                add_signal(
                    HEUR_NEW_EXEC_IN_SENSITIVE_PATH,
                    0.85,
                    {"path": path_str, "ext": ext, "mtime": mtime, "size": size},
                )

            if is_modified and in_sensitive_path:
                add_signal(
                    HEUR_UNEXPECTED_MODIFICATION_NEAR_WALLET,
                    0.8,
                    {"path": path_str, "mtime": mtime, "size": size},
                )

            if known_bad_hashes and ext in suspicious_extensions and size <= (10 * 1024 * 1024):
                sha = _file_sha256(full)
                if sha:
                    files_hashed += 1
                    if sha in known_bad_hashes:
                        add_signal(
                            IOC_HASH_KNOWN_MALICIOUS,
                            0.99,
                            {"path": path_str, "sha256": sha, "source": "Local ThreatDB"},
                        )

        if partial:
            break

    if partial:
        add_signal(
            ENV_SCANNER_PARTIAL_RESULTS,
            0.9,
            {
                "max_scan_seconds": max_scan_seconds,
                "max_files": max_files,
                "files_scanned": files_scanned,
            },
        )

    return FilesystemDetectorResult(
        files_scanned=files_scanned,
        files_hashed=files_hashed,
        partial=partial,
    )
