#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
═══════════════════════════════════════════════════════════════════════════════
 ARGUS — Validateur d'encodage des fichiers
 Détecte BOM, mojibake et fichiers single-line corrompus
═══════════════════════════════════════════════════════════════════════════════

Usage:
  python validate_encoding.py                # Valide tous les fichiers du projet
  python validate_encoding.py --staged       # Valide uniquement les fichiers staged
  python validate_encoding.py --quiet        # Mode silencieux (exit code uniquement)
  python validate_encoding.py --fix          # Retire le BOM des fichiers affectés

Exit codes:
  0 = Aucun problème d'encodage détecté
  1 = Problèmes d'encodage détectés
  2 = Erreur d'exécution du script
"""

import os
import sys
import argparse
import subprocess
from pathlib import Path
from typing import List, Dict, Optional


# ═══════════════════════════════════════════════════════════════════════════════
#  Patterns de corruption connus
#  Scénario type : un éditeur Windows ouvre un fichier UTF-8 en CP850,
#  puis le ré-enregistre en UTF-8 → double-encodage (mojibake).
#  Réf : incident INC-20260221-002 (corruption BOM+CP850 détectée en session 7)
# ═══════════════════════════════════════════════════════════════════════════════

# BOM UTF-8 (EF BB BF) : marqueur optionnel ajouté par certains éditeurs Windows.
# Les parsers YAML le tolèrent, mais il peut gêner d'autres outils.
BOM_UTF8 = b'\xef\xbb\xbf'

# Signatures mojibake : chaque clé est la séquence d'octets résultant du
# double-encodage UTF-8 → CP850 → UTF-8 d'un caractère Unicode spécifique.
# Si on les retrouve dans un fichier, c'est une preuve de corruption.
MOJIBAKE_PATTERNS = {
    b'\xc3\x94\xc3\xb2\xc3\x89': '═ (box-drawing double-bar)',
    b'\xc3\x94\xc3\x87\xc3\xb6': '— (em-dash)',
    b'\xc3\xa2\xc2\x94\xc2\x80': '─ (box-drawing single)',
    b'\xc3\xa2\xc2\x94\xc2\x82': '│ (box-drawing vertical)',
    b'\xc3\xa2\xc2\x94\xc2\x8c': '┌ (box-drawing corner)',
    b'\xc3\xa2\xc2\x94\xc2\x94': '└ (box-drawing corner)',
    b'\xc3\xa2\xc2\x94\xc2\x98': '┘ (box-drawing corner)',
    b'\xc3\xa2\xc2\x94\xc2\x9c': '├ (box-drawing tee)',
}

# Extensions à vérifier
TARGET_EXTENSIONS = {'.yaml', '.yml', '.py', '.ps1', '.json', '.md', '.sh'}

# Un fichier > 200 octets sur une seule ligne est suspect :
# signe d'une concaténation involontaire ou d'une corruption binaire
SINGLE_LINE_MIN_SIZE = 200  # bytes


class EncodingValidator:
    """Validateur d'encodage pour les fichiers du projet Argus."""

    def __init__(self, base_dir: Path, verbose: bool = True):
        self.base_dir = base_dir
        self.verbose = verbose
        self.issues: List[Dict] = []
        self.checked = 0

    def log(self, message: str, force: bool = False):
        if self.verbose or force:
            print(message)

    def check_file(self, filepath: Path) -> List[Dict]:
        """Vérifie un fichier pour les problèmes d'encodage connus."""
        file_issues = []

        try:
            raw = filepath.read_bytes()
        except OSError as e:
            file_issues.append({
                'file': filepath,
                'type': 'read_error',
                'detail': str(e),
            })
            return file_issues

        rel = filepath.relative_to(self.base_dir)
        self.checked += 1

        # 1. Détection BOM
        if raw[:3] == BOM_UTF8:
            file_issues.append({
                'file': filepath,
                'type': 'bom',
                'detail': 'UTF-8 BOM detected (EF BB BF)',
            })

        # 2. Détection mojibake (double-encoding CP850)
        for pattern, description in MOJIBAKE_PATTERNS.items():
            if pattern in raw:
                file_issues.append({
                    'file': filepath,
                    'type': 'mojibake',
                    'detail': f'CP850 double-encoding detected: {description}',
                })
                break  # Un seul avertissement mojibake par fichier

        # 3. Détection fichier single-line suspect
        if len(raw) > SINGLE_LINE_MIN_SIZE:
            # Ignorer le BOM pour le comptage
            content = raw[3:] if raw[:3] == BOM_UTF8 else raw
            newlines = content.count(b'\n')
            if newlines == 0:
                file_issues.append({
                    'file': filepath,
                    'type': 'single_line',
                    'detail': f'File has {len(raw)} bytes but 0 newlines — likely corrupted',
                })

        return file_issues

    def validate_files(self, files: List[Path]) -> int:
        """
        Valide une liste de fichiers.
        Returns: nombre de fichiers avec problèmes.
        """
        for filepath in files:
            issues = self.check_file(filepath)
            if issues:
                self.issues.extend(issues)
                for issue in issues:
                    rel = filepath.relative_to(self.base_dir)
                    icon = {'bom': '🔤', 'mojibake': '💀', 'single_line': '📄', 'read_error': '❌'}
                    self.log(f"  {icon.get(issue['type'], '?')} {issue['type'].upper()}: {rel}")
                    self.log(f"    → {issue['detail']}")

        return len(set(i['file'] for i in self.issues))

    def get_project_files(self) -> List[Path]:
        """Retourne tous les fichiers du projet à vérifier."""
        files = []
        for f in self.base_dir.rglob('*'):
            if '.git' in f.parts:
                continue
            if f.is_file() and f.suffix in TARGET_EXTENSIONS:
                files.append(f)
        return sorted(files)

    def get_staged_files(self) -> List[Path]:
        """Retourne les fichiers staged dans Git."""
        try:
            result = subprocess.run(
                ['git', 'diff', '--cached', '--name-only', '--diff-filter=ACM'],
                capture_output=True, text=True,
                cwd=str(self.base_dir)
            )
            if result.returncode != 0:
                return []

            files = []
            for line in result.stdout.strip().split('\n'):
                if not line:
                    continue
                filepath = self.base_dir / line
                if filepath.exists() and filepath.suffix in TARGET_EXTENSIONS:
                    files.append(filepath)
            return files
        except Exception:
            return []

    def fix_bom(self) -> int:
        """Retire le BOM des fichiers affectés."""
        fixed = 0
        for issue in self.issues:
            if issue['type'] == 'bom':
                filepath = issue['file']
                raw = filepath.read_bytes()
                if raw[:3] == BOM_UTF8:
                    filepath.write_bytes(raw[3:])
                    rel = filepath.relative_to(self.base_dir)
                    self.log(f"  ✓ BOM removed: {rel}")
                    fixed += 1
        return fixed

    def print_report(self):
        """Affiche le rapport de validation."""
        self.log(f"\n{'═' * 60}")
        self.log(f"📊 ENCODING VALIDATION REPORT")
        self.log(f"{'═' * 60}")
        self.log(f"  Files checked: {self.checked}")

        affected_files = set(i['file'] for i in self.issues)
        bom_count = len([i for i in self.issues if i['type'] == 'bom'])
        mojibake_count = len([i for i in self.issues if i['type'] == 'mojibake'])
        single_count = len([i for i in self.issues if i['type'] == 'single_line'])

        self.log(f"  Issues found: {len(self.issues)}")

        if self.issues:
            self.log(f"\n{'─' * 60}")
            if bom_count:
                self.log(f"  🔤 BOM detected: {bom_count} file(s)")
            if mojibake_count:
                self.log(f"  💀 Mojibake detected: {mojibake_count} file(s)")
            if single_count:
                self.log(f"  📄 Single-line suspects: {single_count} file(s)")
            self.log(f"{'─' * 60}")
            self.log(f"\n❌ Encoding problems detected!", force=True)
        else:
            self.log(f"\n✅ No encoding issues found!", force=True)

        self.log(f"{'═' * 60}\n")


# ═══════════════════════════════════════════════════════════════════════════════
#  Utilitaires
# ═══════════════════════════════════════════════════════════════════════════════

def find_project_root() -> Path:
    """Trouve la racine du projet Argus."""
    current = Path(__file__).resolve().parent
    while current != current.parent:
        if (current / 'argus.persona.yaml').exists() and (current / 'memory').is_dir():
            return current
        current = current.parent
    return Path(__file__).resolve().parent.parent


def main():
    parser = argparse.ArgumentParser(
        description='Validate file encoding for Argus memory system',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=__doc__
    )

    parser.add_argument(
        '--staged', '-s',
        action='store_true',
        help='Only validate staged files (for pre-commit hook)'
    )

    parser.add_argument(
        '--quiet', '-q',
        action='store_true',
        help='Quiet mode - only show errors and exit code'
    )

    parser.add_argument(
        '--fix',
        action='store_true',
        help='Automatically remove BOM from affected files'
    )

    args = parser.parse_args()

    try:
        project_root = find_project_root()
    except Exception:
        print("❌ Cannot find Argus project root", file=sys.stderr)
        sys.exit(2)

    verbose = not args.quiet
    validator = EncodingValidator(project_root, verbose=verbose)

    if verbose:
        print(f"\n{'═' * 60}")
        print(f"🔍 ARGUS ENCODING VALIDATOR")
        print(f"{'═' * 60}")
        print(f"  Project root: {project_root}")

    if args.staged:
        files = validator.get_staged_files()
        if verbose:
            print(f"\n🔍 Validating {len(files)} staged file(s)...\n")
        if not files:
            if verbose:
                print("ℹ No staged files to validate.")
            sys.exit(0)
    else:
        files = validator.get_project_files()
        if verbose:
            print(f"\n🔍 Validating {len(files)} file(s)...\n")

    affected = validator.validate_files(files)

    if args.fix and validator.issues:
        fixed = validator.fix_bom()
        if fixed:
            print(f"\n🔧 Fixed {fixed} BOM issue(s).")

    validator.print_report()

    sys.exit(1 if validator.issues else 0)


if __name__ == '__main__':
    main()
