from __future__ import annotations

import hashlib
import platform
import re
import subprocess
import time
from collections import deque
from dataclasses import dataclass
from typing import Iterable

from PIL import ImageGrab
import pytesseract

from .classifier import SafetyClassifier
from .models import Detection, Profile


@dataclass(slots=True)
class ScanResult:
    detections: list[Detection]
    screenshot_size: tuple[int, int]
    elapsed_seconds: float
    app_hint: str
    ocr_line_count: int


class DetectionDeduplicator:
    def __init__(self) -> None:
        self.seen: dict[str, float] = {}

    @staticmethod
    def fingerprint(detection: Detection) -> str:
        normalized = re.sub(r"\W+", " ", detection.text.lower()).strip()
        payload = f"{detection.category}|{normalized}".encode("utf-8")
        return hashlib.sha256(payload).hexdigest()[:20]

    def filter_new(self, detections: list[Detection], cooldown_seconds: int) -> list[Detection]:
        if cooldown_seconds <= 0:
            return detections
        now = time.time()
        self.seen = {key: stamp for key, stamp in self.seen.items() if now - stamp < cooldown_seconds * 2}
        fresh: list[Detection] = []
        for detection in detections:
            key = self.fingerprint(detection)
            detection.detection_id = key
            last_seen = self.seen.get(key)
            if last_seen is None or now - last_seen >= cooldown_seconds:
                fresh.append(detection)
            self.seen[key] = now
        return fresh


class ScreenScanner:
    def __init__(self, classifier: SafetyClassifier) -> None:
        self.classifier = classifier
        self.deduplicator = DetectionDeduplicator()

    @staticmethod
    def active_application() -> str:
        if platform.system() != "Darwin":
            return ""
        script = (
            'tell application "System Events" to get name of first application process whose frontmost is true'
        )
        try:
            result = subprocess.run(
                ["osascript", "-e", script],
                capture_output=True,
                text=True,
                timeout=2,
            )
            if result.returncode == 0:
                return result.stdout.strip()
        except Exception:
            pass
        return ""

    @staticmethod
    def _group_ocr_lines(data: dict) -> Iterable[tuple[str, tuple[int, int, int, int], float]]:
        groups: dict[tuple[int, int, int], list[int]] = {}
        count = len(data.get("text", []))
        for index in range(count):
            text = str(data["text"][index]).strip()
            try:
                confidence = float(data.get("conf", ["-1"] * count)[index])
            except (ValueError, TypeError):
                confidence = -1
            if not text or confidence < 35:
                continue
            key = (
                int(data["block_num"][index]),
                int(data["par_num"][index]),
                int(data["line_num"][index]),
            )
            groups.setdefault(key, []).append(index)

        for indices in groups.values():
            words = [str(data["text"][i]).strip() for i in indices]
            line = " ".join(word for word in words if word)
            left = min(int(data["left"][i]) for i in indices)
            top = min(int(data["top"][i]) for i in indices)
            right = max(int(data["left"][i]) + int(data["width"][i]) for i in indices)
            bottom = max(int(data["top"][i]) + int(data["height"][i]) for i in indices)
            confidences: list[float] = []
            for i in indices:
                try:
                    confidences.append(float(data["conf"][i]))
                except (ValueError, TypeError):
                    pass
            average_confidence = sum(confidences) / len(confidences) if confidences else 0.0
            yield line, (left, top, right - left, bottom - top), average_confidence

    def scan(self, profile: Profile, logical_width: int, logical_height: int) -> ScanResult:
        screenshot = ImageGrab.grab(all_screens=True)
        return self.scan_image(
            screenshot,
            profile,
            logical_width=logical_width,
            logical_height=logical_height,
            app_hint=self.active_application(),
        )

    def scan_image(
        self,
        screenshot,
        profile: Profile,
        *,
        logical_width: int | None = None,
        logical_height: int | None = None,
        app_hint: str = "",
    ) -> ScanResult:
        started = time.perf_counter()
        data = pytesseract.image_to_data(screenshot, output_type=pytesseract.Output.DICT)
        pixel_width, pixel_height = screenshot.size
        logical_width = logical_width or pixel_width
        logical_height = logical_height or pixel_height
        scale_x = logical_width / pixel_width if pixel_width else 1.0
        scale_y = logical_height / pixel_height if pixel_height else 1.0

        detections: list[Detection] = []
        ignored_fragments = {
            "safety judge v13",
            "live protection",
            "scan screen now",
            "analyze text",
            "profile settings",
            "choose a profile",
        }
        ocr_line_count = 0
        for line, (x, y, width, height), _confidence in self._group_ocr_lines(data):
            cleaned = " ".join(line.split())
            if len(cleaned) < 7 or cleaned.lower() in ignored_fragments:
                continue
            ocr_line_count += 1
            detection = self.classifier.classify(
                cleaned,
                enabled_categories=profile.enabled_categories,
                source="screen",
            )
            if detection.category == "SAFE" or detection.risk < profile.risk_threshold:
                continue
            detection.app_hint = app_hint
            detection.x = round(x * scale_x)
            detection.y = round(y * scale_y)
            detection.width = max(18, round(width * scale_x))
            detection.height = max(14, round(height * scale_y))
            detections.append(detection)

        detections = self.deduplicator.filter_new(detections, profile.repeat_cooldown_seconds)
        elapsed = time.perf_counter() - started
        return ScanResult(
            detections=detections,
            screenshot_size=screenshot.size,
            elapsed_seconds=elapsed,
            app_hint=app_hint,
            ocr_line_count=ocr_line_count,
        )
