from __future__ import annotations

import argparse
import hashlib
import json
import os
import re
import uuid
from datetime import datetime
from pathlib import Path
from urllib.parse import quote

ROOT = Path(__file__).resolve().parent.parent
ATTACHMENTS = ROOT / "attachments"
MANIFEST = ROOT / "attachment-rename-manifest.json"
TEXT_EXTENSIONS = {".md", ".canvas", ".base", ".json", ".css"}
EXCLUDED_PARTS = {".trash", ".smart-env", "attachments", "graphify-out"}
NORMALIZED_RE = re.compile(
    r"^(?:img|pdf|audio|video|file)-\d{8}-\d{6}-[0-9a-f]{8}(?:-\d+)?\.[^.]+$",
    re.IGNORECASE,
)
TIMESTAMP_RE = re.compile(
    r"(?<!\d)(20\d{2})(0[1-9]|1[0-2])([0-2]\d|3[01])([0-2]\d)([0-5]\d)([0-5]\d)(?!\d)"
)


def file_hash(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def timestamp_for(path: Path) -> tuple[str, str]:
    matches = list(TIMESTAMP_RE.finditer(path.stem))
    if matches:
        match = matches[-1]
        raw = "".join(match.groups())
        try:
            value = datetime.strptime(raw, "%Y%m%d%H%M%S")
            return value.strftime("%Y%m%d-%H%M%S"), "filename"
        except ValueError:
            pass
    value = datetime.fromtimestamp(path.stat().st_mtime)
    return value.strftime("%Y%m%d-%H%M%S"), "modified-time"


def prefix_for(path: Path) -> str:
    extension = path.suffix.lower()
    if extension in {".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".bmp", ".tif", ".tiff"}:
        return "img"
    if extension == ".pdf":
        return "pdf"
    if extension in {".mp3", ".wav", ".m4a", ".ogg", ".flac"}:
        return "audio"
    if extension in {".mp4", ".mov", ".avi", ".mkv", ".webm"}:
        return "video"
    return "file"


def text_files() -> list[Path]:
    result = []
    for path in ROOT.rglob("*"):
        if not path.is_file() or path.suffix.lower() not in TEXT_EXTENSIONS:
            continue
        if path == MANIFEST:
            continue
        relative = path.relative_to(ROOT)
        if any(part in EXCLUDED_PARTS for part in relative.parts):
            continue
        result.append(path)
    return result


def read_exact(path: Path) -> str:
    with path.open("r", encoding="utf-8", errors="replace", newline="") as handle:
        return handle.read()


def write_exact(path: Path, content: str) -> None:
    with path.open("w", encoding="utf-8", newline="") as handle:
        handle.write(content)


def build_plan() -> tuple[list[dict], list[Path]]:
    candidates = [
        path
        for path in ATTACHMENTS.iterdir()
        if path.is_file() and path.stat().st_size > 0 and not NORMALIZED_RE.match(path.name)
    ]
    texts = text_files()
    text_cache = {path: read_exact(path) for path in texts}
    reserved = {path.name.casefold() for path in ATTACHMENTS.iterdir() if path.is_file() and path not in candidates}
    plan = []

    for path in sorted(candidates, key=lambda item: item.name.casefold()):
        digest = file_hash(path)
        timestamp, timestamp_source = timestamp_for(path)
        stem = f"{prefix_for(path)}-{timestamp}-{digest[:8]}"
        extension = path.suffix.lower()
        new_name = f"{stem}{extension}"
        counter = 2
        while new_name.casefold() in reserved:
            new_name = f"{stem}-{counter}{extension}"
            counter += 1
        reserved.add(new_name.casefold())
        encoded = quote(path.name)
        references = [
            str(text_path.relative_to(ROOT)).replace("\\", "/")
            for text_path, content in text_cache.items()
            if path.name in content or encoded in content
        ]
        plan.append(
            {
                "old": f"attachments/{path.name}",
                "new": f"attachments/{new_name}",
                "sha256": digest,
                "size": path.stat().st_size,
                "timestamp_source": timestamp_source,
                "references": references,
            }
        )
    return plan, texts


def execute(plan: list[dict], texts: list[Path]) -> dict:
    temporary = []
    for entry in plan:
        source = ROOT / entry["old"]
        temp = ATTACHMENTS / f".normalize-{uuid.uuid4().hex}{source.suffix.lower()}"
        source.rename(temp)
        temporary.append((temp, ATTACHMENTS / Path(entry["new"]).name))

    for temp, destination in temporary:
        temp.rename(destination)

    replacements = []
    for entry in plan:
        old_name = Path(entry["old"]).name
        new_name = Path(entry["new"]).name
        replacements.extend(
            [
                (old_name, new_name),
                (quote(old_name), quote(new_name)),
            ]
        )

    changed_files = 0
    replacement_count = 0
    for path in texts:
        original = read_exact(path)
        updated = original
        for old, new in replacements:
            count = updated.count(old)
            if count:
                replacement_count += count
                updated = updated.replace(old, new)
        if updated != original:
            write_exact(path, updated)
            changed_files += 1

    return {
        "renamed": len(plan),
        "changed_text_files": changed_files,
        "reference_replacements": replacement_count,
    }


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--apply", action="store_true")
    args = parser.parse_args()

    plan, texts = build_plan()
    zero_byte = [path.name for path in ATTACHMENTS.iterdir() if path.is_file() and path.stat().st_size == 0]
    manifest = {
        "created_at": datetime.now().isoformat(timespec="seconds"),
        "root": str(ROOT),
        "applied": args.apply,
        "zero_byte_untouched": zero_byte,
        "renames": plan,
    }
    MANIFEST.write_text(json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8")

    result = {
        "mode": "apply" if args.apply else "dry-run",
        "planned_renames": len(plan),
        "referenced_renames": sum(bool(item["references"]) for item in plan),
        "zero_byte_untouched": len(zero_byte),
        "manifest": str(MANIFEST),
    }
    if args.apply:
        result.update(execute(plan, texts))
    print(json.dumps(result, indent=2))


if __name__ == "__main__":
    main()
