from __future__ import annotations

import fnmatch
import os
import re
from pathlib import Path
from typing import Any

from aicd.config import PathPolicyConfig
from aicd.tools.path_policy import PathPolicy


class FSTool:
    def __init__(self, policy: PathPolicy, cwd: Path | None = None) -> None:
        self._policy = policy
        self._cwd = cwd or Path.cwd()
        self._cfg: PathPolicyConfig = policy._cfg  # noqa: SLF001

    def _p(self, path_str: str) -> Path:
        p = self._policy.resolve(path_str, self._cwd)
        self._policy.check_allowed(p)
        return p

    def list_dir(self, path: str) -> dict[str, Any]:
        d = self._p(path)
        if not d.is_dir():
            return {"ok": False, "error": "not a directory"}
        entries = []
        for name in sorted(d.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower())):
            try:
                st = name.stat()
                entries.append(
                    {
                        "name": name.name,
                        "path": str(name),
                        "is_dir": name.is_dir(),
                        "size": st.st_size if name.is_file() else None,
                    }
                )
            except OSError as e:
                entries.append({"name": name.name, "path": str(name), "error": str(e)})
        return {"ok": True, "path": str(d), "entries": entries}

    def read_file(self, path: str, max_bytes: int | None = None) -> dict[str, Any]:
        p = self._p(path)
        if not p.is_file():
            return {"ok": False, "error": "not a file"}
        limit = max_bytes or self._cfg.max_read_bytes
        data = p.read_bytes()
        truncated = len(data) > limit
        chunk = data[:limit]
        text: str | None
        try:
            text = chunk.decode("utf-8")
        except UnicodeDecodeError:
            text = None
        return {
            "ok": True,
            "path": str(p),
            "truncated": truncated,
            "size": len(data),
            "text": text,
            "binary_preview_hex": None
            if text is not None
            else (chunk[:128].hex() + ("..." if len(chunk) > 128 else "")),
        }

    def read_bytes_full(self, path: str, max_bytes: int | None = None) -> dict[str, Any]:
        p = self._p(path)
        if not p.is_file():
            return {"ok": False, "error": "not a file"}
        limit = max_bytes or self._cfg.max_read_bytes * 10
        data = p.read_bytes()
        if len(data) > limit:
            return {"ok": False, "error": f"file too large ({len(data)} > {limit})"}
        return {"ok": True, "path": str(p), "data": data}

    def write_bytes(self, path: str, data: bytes) -> dict[str, Any]:
        p = self._p(path)
        p.parent.mkdir(parents=True, exist_ok=True)
        p.write_bytes(data)
        return {"ok": True, "path": str(p), "bytes": len(data)}

    def write_file(self, path: str, content: str, encoding: str = "utf-8") -> dict[str, Any]:
        p = self._p(path)
        p.parent.mkdir(parents=True, exist_ok=True)
        p.write_text(content, encoding=encoding)
        return {"ok": True, "path": str(p), "bytes": len(content.encode(encoding))}

    def delete_path(self, path: str, dry_run: bool = False) -> dict[str, Any]:
        p = self._p(path)
        if dry_run:
            return {"ok": True, "dry_run": True, "would_delete": str(p)}
        if p.is_dir():
            import shutil

            shutil.rmtree(p)
        elif p.is_file():
            p.unlink()
        else:
            return {"ok": False, "error": "path does not exist"}
        return {"ok": True, "deleted": str(p)}

    def mkdir(self, path: str, parents: bool = True) -> dict[str, Any]:
        p = self._p(path)
        p.mkdir(parents=parents, exist_ok=True)
        return {"ok": True, "path": str(p)}

    def move(self, src: str, dest: str) -> dict[str, Any]:
        s = self._p(src)
        d = self._p(dest)
        self._policy.check_allowed(d)
        d.parent.mkdir(parents=True, exist_ok=True)
        os.replace(s, d)
        return {"ok": True, "from": str(s), "to": str(d)}

    def glob_names(self, directory: str, pattern: str) -> dict[str, Any]:
        d = self._p(directory)
        if not d.is_dir():
            return {"ok": False, "error": "not a directory"}
        matches = []
        for root, _dirs, files in os.walk(d):
            rp = Path(root)
            for f in files:
                if fnmatch.fnmatch(f, pattern):
                    matches.append(str(rp / f))
        return {"ok": True, "count": len(matches), "paths": matches[:500]}

    def search_content_regex(
        self,
        directory: str,
        pattern: str,
        *,
        glob_filter: str = "*",
        max_files: int = 200,
        max_matches_per_file: int = 50,
    ) -> dict[str, Any]:
        d = self._p(directory)
        if not d.is_dir():
            return {"ok": False, "error": "not a directory"}
        try:
            rx = re.compile(pattern)
        except re.error as e:
            return {"ok": False, "error": f"invalid regex: {e}"}
        results: list[dict[str, Any]] = []
        files_seen = 0
        for root, _dirs, files in os.walk(d):
            for fname in files:
                if not fnmatch.fnmatch(fname, glob_filter):
                    continue
                fp = Path(root) / fname
                self._policy.check_allowed(fp)
                if not fp.is_file():
                    continue
                try:
                    if fp.stat().st_size > self._cfg.max_search_file_bytes:
                        continue
                except OSError:
                    continue
                files_seen += 1
                if files_seen > max_files:
                    break
                try:
                    raw = fp.read_bytes()
                    if b"\x00" in raw[:8192]:
                        continue
                    text = raw.decode("utf-8", errors="replace")
                except OSError:
                    continue
                file_matches: list[dict[str, Any]] = []
                for m in rx.finditer(text):
                    file_matches.append(
                        {
                            "start": m.start(),
                            "end": m.end(),
                            "match": m.group(0)[:200],
                        }
                    )
                    if len(file_matches) >= max_matches_per_file:
                        break
                if file_matches:
                    results.append({"path": str(fp), "matches": file_matches})
            if files_seen > max_files:
                break
        return {"ok": True, "results": results, "files_scanned": files_seen}
