"""Ingest worker: deduplication, classification, sorting of claimed files.

Takes files from processing/<batch_id>/ (claimed by claim_batch), deduplicates
by sha256 against manifest, classifies by extension/content, moves to
sorted/<category>/ or duplicates/, and updates the manifest.
"""

from __future__ import annotations

import hashlib
import json
import logging
import time
from pathlib import Path
from uuid import uuid4

from cyberbrain26.product.shared_root import manifest_write, resolve_shared_root

logger = logging.getLogger(__name__)

__all__ = [
    "classify_file",
    "compute_sha256",
    "is_ai_export",
    "process_batch",
    "read_manifest_entries",
]

# Kategorie souborů pro třídění
CATEGORY_AI_EXPORT = "ai-export"
CATEGORY_NOTE = "note"
CATEGORY_DOC = "doc"
CATEGORY_CODE = "code"
CATEGORY_MEDIA = "media"
CATEGORY_ARCHIVE = "archive"
CATEGORY_OTHER = "other"

# Přípony AI exportů (konverzace z ChatGPT, Gemini, Perplexity atd.)
AI_EXPORT_PATTERNS = (
    "conversations", "chat", "user", "assistant", "messages",
    "perplexity", "gemini", "claude", "gpt", "openai",
)

# Přípony podle kategorie
EXT_NOTE = (".md", ".txt", ".rst", ".org")
EXT_DOC = (".pdf", ".doc", ".docx", ".odt", ".rtf")
EXT_CODE = (".py", ".js", ".ts", ".java", ".cpp", ".c", ".h", ".go", ".rs", ".sh", ".bat", ".ps1", ".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf")
EXT_MEDIA = (".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", ".webp", ".ico", ".mp4", ".mov", ".avi", ".mkv", ".mp3", ".wav", ".flac", ".ogg", ".zip", ".tar", ".gz", ".bz2", ".7z", ".rar")


def compute_sha256(path: Path) -> str:
    """Vypočítá SHA-256 hash souboru."""
    h = hashlib.sha256()
    with path.open("rb") as f:
        for chunk in iter(lambda: f.read(65536), b""):
            h.update(chunk)
    return h.hexdigest()


def read_manifest_entries(shared_root: Path) -> list[dict]:
    """Přečte všechny záznamy z manifestu (prázdný seznam pokud neexistuje)."""
    mpath = shared_root / "manifest" / "ingest-manifest.jsonl"
    if not mpath.exists():
        return []
    entries: list[dict] = []
    with open(mpath, encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if line:
                try:
                    entries.append(json.loads(line))
                except json.JSONDecodeError:
                    continue
    return entries


def is_ai_export(path: Path) -> bool:
    """Detekuje zda je soubor AI export (konverzace z ChatGPT/Gemini/Perplexity atd.)."""
    name = path.name.lower()
    # Kontrola názvu souboru
    for pat in AI_EXPORT_PATTERNS:
        if pat in name:
            return True
    # Kontrola obsahu pro JSON konverzace (první KB)
    if path.suffix.lower() == ".json":
        try:
            with open(path, encoding="utf-8", errors="replace") as f:
                head = f.read(2048).lower()
                for pat in ("conversation", "messages", "assistant", "user", "gemini", "perplexity", "chatgpt"):
                    if pat in head:
                        return True
        except OSError:
            pass
    return False


def classify_file(path: Path) -> str:
    """Klasifikuje soubor podle přípony a obsahu. Vrací kategorii."""
    if is_ai_export(path):
        return CATEGORY_AI_EXPORT
    ext = path.suffix.lower()
    if ext in EXT_NOTE:
        return CATEGORY_NOTE
    if ext in EXT_DOC:
        return CATEGORY_DOC
    if ext in EXT_CODE:
        return CATEGORY_CODE
    if ext in EXT_MEDIA:
        return CATEGORY_MEDIA
    if ext in (".zip", ".tar", ".gz", ".bz2", ".7z", ".rar"):
        return CATEGORY_ARCHIVE
    return CATEGORY_OTHER


def process_batch(
    batch_id: str,
    shared_root: str | Path | None = None,
) -> dict:
    """Zpracuje všechny soubory v processing/<batch_id>/.

    Pro každý soubor:
    1. Vypočítá sha256
    2. Zkontroluje deduplikaci proti existujícím záznamům v manifestu
    3. Klasifikuje
    4. Přesune do sorted/<kategorie>/ nebo duplicates/ (pokud duplikát)
    5. Aktualizuje manifest

    Vrací dict s výsledky.
    """
    root = Path(shared_root).expanduser().resolve() if shared_root else resolve_shared_root()
    processing_dir = root / "processing" / batch_id
    sorted_dir = root / "sorted"
    duplicates_dir = root / "duplicates"

    if not processing_dir.exists():
        return {"batch_id": batch_id, "processed": 0, "duplicates": 0, "errors": []}

    existing_entries = read_manifest_entries(root)
    existing_hashes = {
        e.get("sha256") for e in existing_entries
        if e.get("sha256") and e.get("status") in ("classified", "indexed", "duplicate")
    }

    processed: list[dict] = []
    duplicates: list[dict] = []
    errors: list[str] = []

    for entry in sorted(processing_dir.iterdir()):
        if not entry.is_file():
            continue

        sha256 = compute_sha256(entry)
        category = classify_file(entry)

        # Deduplikace
        if sha256 in existing_hashes:
            # Je to duplikát -> přesun do duplicates/
            dst = duplicates_dir / entry.name
            # Pokud soubor se stejným názvem už existuje, přidáme suffix
            if dst.exists():
                stem = entry.stem
                suffix = entry.suffix
                i = 1
                while dst.exists():
                    dst = duplicates_dir / f"{stem}_{i}{suffix}"
                    i += 1
            try:
                entry.replace(dst)
                duplicates.append({
                    "src": str(entry.relative_to(root)),
                    "dst": str(dst.relative_to(root)),
                    "sha256": sha256,
                    "category": category,
                })
                manifest_write(
                    {
                        "id": uuid4().hex,
                        "batch_id": batch_id,
                        "ts": time.time(),
                        "src_path_rel": str(entry.relative_to(root)),
                        "dst_path_rel": str(dst.relative_to(root)),
                        "sha256": sha256,
                        "size": dst.stat().st_size,
                        "mime_or_ext": entry.suffix.lower(),
                        "type": category,
                        "status": "duplicate",
                        "worker": "ingest_worker",
                        "reason": "duplicate_content",
                        "dup_of_sha256": sha256,
                        "task_hits": 0,
                    },
                    shared_root=root,
                )
                existing_hashes.add(sha256)
                continue
            except OSError as exc:
                logger.warning("Could not move duplicate %s: %s", entry, exc)
                errors.append(str(entry))
                continue

        # Normální zpracování -> přesun do sorted/<kategorie>/
        cat_dir = sorted_dir / category
        cat_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
        dst = cat_dir / entry.name
        if dst.exists():
            stem = entry.stem
            suffix = entry.suffix
            i = 1
            while dst.exists():
                dst = cat_dir / f"{stem}_{i}{suffix}"
                i += 1
        try:
            entry.replace(dst)
            processed.append({
                "src": str(entry.relative_to(root)),
                "dst": str(dst.relative_to(root)),
                "sha256": sha256,
                "category": category,
            })
            manifest_write(
                {
                    "id": uuid4().hex,
                    "batch_id": batch_id,
                    "ts": time.time(),
                    "src_path_rel": str(entry.relative_to(root)),
                    "dst_path_rel": str(dst.relative_to(root)),
                    "sha256": sha256,
                    "size": dst.stat().st_size,
                    "mime_or_ext": entry.suffix.lower(),
                    "type": category,
                    "status": "classified",
                    "worker": "ingest_worker",
                    "reason": None,
                    "dup_of_sha256": None,
                    "task_hits": 0,
                },
                shared_root=root,
            )
            existing_hashes.add(sha256)
        except OSError as exc:
            logger.warning("Could not move %s: %s", entry, exc)
            errors.append(str(entry))

    return {
        "batch_id": batch_id,
        "processed": len(processed),
        "duplicates": len(duplicates),
        "processed_files": processed,
        "duplicate_files": duplicates,
        "errors": errors,
    }
