from __future__ import annotations

import hashlib
import os
import re
from dataclasses import dataclass
from typing import Iterable


TEXTUAL_EXTENSIONS = {
    ".py",
    ".js",
    ".jsx",
    ".ts",
    ".tsx",
    ".java",
    ".kt",
    ".go",
    ".rs",
    ".c",
    ".h",
    ".cpp",
    ".hpp",
    ".cs",
    ".php",
    ".rb",
    ".swift",
    ".scala",
    ".sql",
    ".json",
    ".yml",
    ".yaml",
    ".toml",
    ".ini",
    ".cfg",
    ".md",
    ".txt",
    ".sh",
    ".ps1",
    ".dockerfile",
    ".gradle",
    ".xml",
    ".html",
    ".css",
    ".scss",
    ".less",
}


def sha256_file(path: str, chunk_size: int = 1024 * 1024) -> str:
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(chunk_size), b""):
            h.update(chunk)
    return h.hexdigest()


def is_probably_binary(path: str, sniff_bytes: int = 8192) -> bool:
    try:
        with open(path, "rb") as f:
            b = f.read(sniff_bytes)
    except OSError:
        return True
    if not b:
        return False
    if b"\x00" in b:
        return True
    # Heuristic: too many non-text bytes
    # Allow common whitespace bytes; treat the rest as printable ASCII-ish.
    text = sum(1 for x in b if x in b"\n\r\t\f\b" or 32 <= x <= 126)
    return text / len(b) < 0.80


def is_textual_extension(path: str) -> bool:
    name = os.path.basename(path).lower()
    if name == "dockerfile":
        return True
    _, ext = os.path.splitext(name)
    return ext in TEXTUAL_EXTENSIONS


def read_text_file(path: str, max_bytes: int | None = None) -> str:
    """
    Best-effort text read.
    - Uses UTF-8 with replacement fallback.
    - max_bytes can cap very large reads (still useful for excerpting).
    """
    with open(path, "rb") as f:
        data = f.read() if max_bytes is None else f.read(max_bytes)
    # Try UTF-8 first
    try:
        return data.decode("utf-8")
    except UnicodeDecodeError:
        return data.decode("utf-8", errors="replace")


_WS_RE = re.compile(r"\s+")


def normalize_whitespace(s: str) -> str:
    return _WS_RE.sub(" ", s).strip()


@dataclass(frozen=True)
class TokenEstimate:
    chars: int
    approx_tokens: int


def estimate_tokens(text: str) -> TokenEstimate:
    # Rough heuristic: 1 token ~= 4 chars for code-ish English; OK for chunking gates.
    chars = len(text)
    return TokenEstimate(chars=chars, approx_tokens=max(1, chars // 4))


def iter_parent_paths(path: str) -> Iterable[str]:
    """
    Yield parent directories from path up to root.
    """
    cur = os.path.abspath(path)
    while True:
        parent = os.path.dirname(cur)
        if parent == cur:
            break
        yield parent
        cur = parent

