"""
Service de détection de fraude sur les documents diplômes.
Analyse les images pour détecter les manipulations (métadonnées, patterns visuels, ELA).

ELA = Error Level Analysis — technique qui révèle les zones retouchées d'une image JPEG.
"""
import io
import logging
import math
import os
import tempfile
from pathlib import Path
from typing import Any

from PIL import Image, ImageChops, ImageEnhance

logger = logging.getLogger(__name__)


class FraudDetectionService:
    """
    Analyse les images de diplômes pour détecter d'éventuelles falsifications.
    Utilise plusieurs heuristiques sans dépendance à TensorFlow (léger et portable).

    Techniques utilisées :
    1. ELA (Error Level Analysis) — détecte les zones recompressées/retouchées
    2. Analyse des métadonnées EXIF
    3. Détection de zones uniformes suspectes (collage)
    4. Analyse de l'histogramme
    """

    ELA_QUALITY = 90       # Qualité de re-compression pour ELA
    ELA_THRESHOLD = 15.0   # Seuil moyen d'erreur ELA (en niveaux de gris)
    HIGH_RISK_THRESHOLD = 0.7
    MEDIUM_RISK_THRESHOLD = 0.4

    def analyze_image(self, image_path: str) -> dict[str, Any]:
        """
        Analyse une image de diplôme et retourne un score de risque de fraude.

        Returns:
            dict avec is_fraud, confidence, risk_level, details, message
        """
        try:
            img = Image.open(image_path).convert("RGB")
        except Exception as exc:
            logger.error("Impossible d'ouvrir l'image : %s", exc)
            return self._error_result(str(exc))

        scores: list[float] = []
        details: dict[str, Any] = {}

        # ── 1. ELA ────────────────────────────────────────────────────────────
        ela_score, ela_info = self._ela_analysis(img)
        scores.append(ela_score)
        details["ela"] = ela_info

        # ── 2. Métadonnées EXIF ───────────────────────────────────────────────
        meta_score, meta_info = self._metadata_analysis(img)
        scores.append(meta_score)
        details["metadata"] = meta_info

        # ── 3. Zones uniformes (collage) ──────────────────────────────────────
        uniform_score, uniform_info = self._uniform_region_analysis(img)
        scores.append(uniform_score)
        details["uniform_regions"] = uniform_info

        # ── 4. Histogramme ────────────────────────────────────────────────────
        hist_score, hist_info = self._histogram_analysis(img)
        scores.append(hist_score)
        details["histogram"] = hist_info

        # ── Score final : moyenne pondérée ────────────────────────────────────
        weights = [0.40, 0.20, 0.25, 0.15]   # ELA a le plus de poids
        confidence = sum(s * w for s, w in zip(scores, weights))
        confidence = round(min(max(confidence, 0.0), 1.0), 4)

        if confidence >= self.HIGH_RISK_THRESHOLD:
            risk_level = "high"
            is_fraud   = True
            message    = "Document suspect — forte probabilité de falsification"
        elif confidence >= self.MEDIUM_RISK_THRESHOLD:
            risk_level = "medium"
            is_fraud   = False
            message    = "Document à vérifier manuellement"
        else:
            risk_level = "low"
            is_fraud   = False
            message    = "Document conforme — aucune anomalie détectée"

        return {
            "is_fraud":   is_fraud,
            "confidence": confidence,
            "risk_level": risk_level,
            "details":    details,
            "message":    message,
        }

    # ─── Techniques d'analyse ────────────────────────────────────────────────

    def _ela_analysis(self, img: Image.Image) -> tuple[float, dict]:
        """
        Error Level Analysis : re-compresse l'image à qualité réduite et
        calcule la différence pixel par pixel.
        Des différences élevées dans des zones localisées indiquent une retouche.
        """
        try:
            with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
                tmp_path = tmp.name
            img.save(tmp_path, "JPEG", quality=self.ELA_QUALITY)
            recompressed = Image.open(tmp_path).convert("RGB")
            os.unlink(tmp_path)

            diff = ImageChops.difference(img, recompressed)
            extrema = diff.getextrema()
            max_diff = max(ch[1] for ch in extrema) if extrema else 0

            # Amplifier pour la visualisation
            amplified = ImageEnhance.Brightness(diff).enhance(10)
            pixels = list(amplified.getdata())
            avg_brightness = sum(sum(p) / 3 for p in pixels) / len(pixels)

            # Normalise vers 0-1 (seuil = ELA_THRESHOLD)
            score = min(avg_brightness / (self.ELA_THRESHOLD * 10), 1.0)

            return score, {
                "avg_brightness": round(avg_brightness, 2),
                "max_diff":       max_diff,
                "suspicious":     avg_brightness > self.ELA_THRESHOLD,
            }
        except Exception as exc:
            logger.warning("ELA échoué : %s", exc)
            return 0.0, {"error": str(exc)}

    def _metadata_analysis(self, img: Image.Image) -> tuple[float, dict]:
        """
        Analyse les métadonnées EXIF pour détecter des incohérences.
        Ex: logiciel d'édition dans les tags, date de modification absente, etc.
        """
        try:
            exif_data = img._getexif() if hasattr(img, "_getexif") else None
            score = 0.0
            flags = []

            if exif_data is None:
                # Absence totale d'EXIF sur un scan de document = léger risque
                flags.append("no_exif")
                score += 0.1
            else:
                # Tag 305 = Software
                software = exif_data.get(305, "")
                editing_tools = ["photoshop", "gimp", "paint", "illustrator", "inkscape"]
                if any(tool in software.lower() for tool in editing_tools):
                    flags.append(f"edited_with_{software}")
                    score += 0.5

                # Tag 306 = DateTime, 36867 = DateTimeOriginal
                dt = exif_data.get(306)
                dto = exif_data.get(36867)
                if dt and dto and dt != dto:
                    flags.append("datetime_mismatch")
                    score += 0.3

            score = min(score, 1.0)
            return score, {"flags": flags, "score": round(score, 4)}
        except Exception as exc:
            return 0.0, {"error": str(exc)}

    def _uniform_region_analysis(self, img: Image.Image) -> tuple[float, dict]:
        """
        Détecte les zones trop uniformes qui pourraient signaler un collage
        (remplacement d'une zone par une couleur unie ou un bloc copié).
        """
        try:
            small = img.resize((64, 64))
            pixels = list(small.getdata())

            # Divise en blocs 8x8 et calcule la variance de chaque bloc
            block_size = 8
            num_blocks = 64 // block_size
            suspicious_blocks = 0
            total_blocks = 0

            for bx in range(num_blocks):
                for by in range(num_blocks):
                    block = []
                    for x in range(block_size):
                        for y in range(block_size):
                            idx = (bx * block_size + x) * 64 + (by * block_size + y)
                            block.append(pixels[idx])

                    # Variance de luminosité
                    brightness = [sum(p) / 3 for p in block]
                    mean = sum(brightness) / len(brightness)
                    variance = sum((b - mean) ** 2 for b in brightness) / len(brightness)

                    if variance < 5.0:   # Bloc quasi-uniforme
                        suspicious_blocks += 1
                    total_blocks += 1

            ratio = suspicious_blocks / total_blocks if total_blocks else 0
            # Un document scané légitimement a des zones uniformes (fond blanc) — seuil à 70%
            score = max(0.0, (ratio - 0.70) * 3.33) if ratio > 0.70 else 0.0

            return round(score, 4), {
                "suspicious_blocks": suspicious_blocks,
                "total_blocks":      total_blocks,
                "ratio":             round(ratio, 4),
            }
        except Exception as exc:
            return 0.0, {"error": str(exc)}

    def _histogram_analysis(self, img: Image.Image) -> tuple[float, dict]:
        """
        Analyse l'histogramme pour détecter des anomalies comme des pics
        anormaux qui indiquent une post-édition numérique.
        """
        try:
            gray = img.convert("L")
            hist = gray.histogram()  # 256 valeurs

            total_pixels = sum(hist)
            if total_pixels == 0:
                return 0.0, {}

            # Normalise
            hist_norm = [h / total_pixels for h in hist]

            # Détecte les pics anormaux (valeur très concentrée sur quelques niveaux)
            peak_threshold = 0.05   # 5% des pixels sur un seul niveau = suspect
            suspicious_peaks = [i for i, v in enumerate(hist_norm) if v > peak_threshold]

            # Calcule l'entropie de Shannon
            entropy = -sum(v * math.log2(v + 1e-10) for v in hist_norm)
            # Un document légitime a une entropie de 6-8 bits
            entropy_anomaly = max(0.0, (6.0 - entropy) / 6.0)  # Entropie trop basse = suspect

            score = min(len(suspicious_peaks) * 0.2 + entropy_anomaly * 0.3, 1.0)

            return round(score, 4), {
                "suspicious_peaks": suspicious_peaks[:5],   # Max 5 pour la lisibilité
                "entropy":          round(entropy, 4),
                "entropy_anomaly":  round(entropy_anomaly, 4),
            }
        except Exception as exc:
            return 0.0, {"error": str(exc)}

    def _error_result(self, message: str) -> dict:
        return {
            "is_fraud":   False,
            "confidence": 0.0,
            "risk_level": "unknown",
            "details":    {},
            "message":    f"Erreur d'analyse : {message}",
        }


# Singleton
fraud_service = FraudDetectionService()
