from __future__ import annotations

import shutil
import tempfile
import zipfile
from pathlib import Path
from typing import Iterable, List, Tuple


def safe_read_text(path: Path, limit: int = 100_000) -> str:
    try:
        with path.open("r", encoding="utf-8", errors="ignore") as f:
            return f.read(limit)
    except OSError:
        return ""


def safe_count_lines(path: Path) -> int:
    try:
        with path.open("r", encoding="utf-8", errors="ignore") as f:
            return sum(1 for _ in f)
    except OSError:
        return 0


def iter_files(root: Path, patterns: Iterable[str], ignore: List[str] | None = None) -> Iterable[Path]:
    ignore = ignore or []
    for pat in patterns:
        for p in (p for p in root.rglob(pat) if p.is_file()):
            if path_is_ignored(p, ignore):
                continue
            yield p


def collect_stats(root: Path, ignore: List[str] | None = None) -> Tuple[dict, int, int, int, int]:
    ignore = ignore or []
    file_types = {}
    total_files = 0
    code_bytes = 0
    loc_php = 0
    loc_js = 0
    loc_css = 0

    for path in root.rglob("*"):
        if not path.is_file():
            continue
        if path_is_ignored(path, ignore):
            continue
        total_files += 1
        suffix = path.suffix.lower() or "no_ext"
        file_types[suffix] = file_types.get(suffix, 0) + 1
        try:
            size = path.stat().st_size
            code_bytes += size
        except OSError:
            pass
        if suffix == ".php":
            loc_php += safe_count_lines(path)
        elif suffix == ".js":
            loc_js += safe_count_lines(path)
        elif suffix == ".css":
            loc_css += safe_count_lines(path)

    return file_types, total_files, code_bytes, loc_php, loc_js, loc_css


def extract_zip_if_needed(target: Path) -> Tuple[Path, Path | None]:
    """
    If target is a zip, extract to temp dir and return (workdir, temp_dir).
    Caller should delete temp_dir when done.
    """
    if target.is_file() and zipfile.is_zipfile(target):
        temp_dir = Path(tempfile.mkdtemp(prefix="ferruccio-"))
        with zipfile.ZipFile(target, "r") as zf:
            zf.extractall(temp_dir)
        workdir = detect_plugin_root(temp_dir)
        return workdir, temp_dir
    elif target.is_dir():
        return detect_plugin_root(target), None
    raise FileNotFoundError(f"{target} is not a directory or zip")


def detect_plugin_root(root: Path) -> Path:
    if root.is_file():
        return root
    immediate_dirs = [p for p in root.iterdir() if p.is_dir() and p.name != "__MACOSX"]
    php_at_top = list(root.glob("*.php"))
    if not php_at_top and len(immediate_dirs) == 1:
        return immediate_dirs[0]
    return root


def cleanup_temp_dir(temp_dir: Path | None) -> None:
    if temp_dir and temp_dir.exists():
        shutil.rmtree(temp_dir, ignore_errors=True)


def path_is_ignored(path: Path, ignore: List[str]) -> bool:
    if not ignore:
        return False
    path_str = str(path)
    for pattern in ignore:
        if not pattern:
            continue
        if path.match(pattern):
            return True
        if pattern in path_str:
            return True
    return False
