from __future__ import annotations

import itertools
import os
import shutil
from collections.abc import Sequence
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Any, ClassVar, Literal

from ..core.engine.contracts import TableColumn, TableSlice
from ..core.plan import QueryPlan
from ..core.sheet import (
    SHEET_FEATURE_PREVIEW,
    SHEET_FEATURE_ROW_COUNT,
    SHEET_FEATURE_SLICE,
    SHEET_FEATURE_VALUE_AT,
    SheetFeature,
)
from ..core.sheet_actions import SheetEnterAction
from ..data.scanners import ScannerRegistry


@dataclass(slots=True, frozen=True)
class FileBrowserAction:
    type: Literal["enter-directory", "open-file"]
    path: Path


@dataclass(slots=True, frozen=True)
class FileDeletionResult:
    deleted: tuple[Path, ...]
    errors: tuple[tuple[Path, str], ...]

    @property
    def changed(self) -> bool:
        return bool(self.deleted)


@dataclass(slots=True)
class _FileBrowserEntry:
    row: dict[str, Any]
    path: Path
    is_dir: bool
    openable: bool


@dataclass(slots=True)
class _FileBrowserSnapshot:
    entries: list[_FileBrowserEntry]
    rows: list[dict[str, Any]]
    signature: tuple[tuple[str, str, str, str, bool], ...]
    error: str | None


class FileBrowserSheet:
    """Sheet that lists openable datasets and directories within a folder."""

    _COLUMNS: ClassVar[tuple[str, ...]] = ("name", "type", "size", "modified")
    _CACHE_EPOCH = itertools.count(1)
    _CAPABILITIES: ClassVar[frozenset[SheetFeature]] = frozenset(
        {
            SHEET_FEATURE_PREVIEW,
            SHEET_FEATURE_SLICE,
            SHEET_FEATURE_VALUE_AT,
            SHEET_FEATURE_ROW_COUNT,
        }
    )

    def __init__(self, directory: Path, *, scanners: ScannerRegistry):
        self.is_file_browser = True
        self._scanners = scanners
        self._plan = QueryPlan()
        self.sheet_id = f"file-browser:{directory}"
        self.directory = self._normalise_path(directory)
        self._schema = {
            "name": str,
            "type": str,
            "size": str,
            "modified": str,
        }
        self._entries: list[_FileBrowserEntry] = []
        self._rows: list[dict[str, Any]] = []
        self._error: str | None = None
        self._entries_signature: tuple[tuple[str, str, str, str, bool], ...] | None = None
        self._cache_version: int = next(self._CACHE_EPOCH)
        snapshot = self._capture_directory_snapshot()
        self._apply_snapshot(snapshot)

    @staticmethod
    def _normalise_path(path: Path) -> Path:
        candidate = Path(path).expanduser()
        if not candidate.is_absolute():
            try:
                base_path: Path | None = None
                base = os.environ.get("PWD")
                if base:
                    base_path = Path(base).expanduser()
                    if not base_path.is_absolute():
                        base_path = None
                cwd = Path.cwd()
                if base_path is not None:
                    try:
                        if base_path.samefile(cwd):
                            candidate = base_path / candidate
                        else:
                            candidate = cwd / candidate
                    except OSError:
                        candidate = cwd / candidate
                else:
                    candidate = cwd / candidate
            except OSError:
                candidate = Path.cwd() / candidate
        try:
            return candidate.absolute()
        except OSError:
            return candidate

    @property
    def columns(self) -> list[str]:
        return list(self._COLUMNS)

    @property
    def display_path(self) -> str:
        return str(self.directory)

    @property
    def status_message(self) -> str | None:
        return self._error

    @property
    def cache_version(self) -> int:
        return self._cache_version

    def schema_dict(self) -> dict[str, Any]:
        return dict(self._schema)

    def plan_snapshot(self) -> dict[str, object]:
        return {"kind": "file-browser", "path": str(self.directory)}

    @property
    def plan(self) -> QueryPlan:
        return self._plan

    def with_plan(self, plan: QueryPlan) -> FileBrowserSheet:
        del plan
        return self

    def _capture_directory_snapshot(self) -> _FileBrowserSnapshot:
        entries: list[_FileBrowserEntry] = []
        rows: list[dict[str, Any]] = []
        fingerprint: list[tuple[str, str, str, str, bool]] = []
        error: str | None = None

        parent = self.directory.parent
        if parent != self.directory:
            entry = self._build_entry(
                name="..",
                path=parent,
                is_dir=True,
                openable=True,
                size_display="",
                modified_display="",
            )
            entries.append(entry)
            rows.append(entry.row)
            fingerprint.append(self._entry_signature(entry))

        try:
            children = sorted(
                self.directory.iterdir(),
                key=lambda path: (not path.is_dir(), path.name.lower()),
            )
        except OSError as exc:
            error = f"dir error: {exc}"
            return _FileBrowserSnapshot(entries, rows, tuple(fingerprint), error)

        dirs: list[_FileBrowserEntry] = []
        files: list[_FileBrowserEntry] = []
        for child in children:
            is_dir = child.is_dir()
            is_openable = self._scanners.can_scan(child)
            if is_dir:
                entry = self._format_entry(child, is_dir=True, openable=True)
                dirs.append(entry)
                continue
            if not child.is_file():
                continue
            if not is_openable:
                continue
            entry = self._format_entry(child, is_dir=False, openable=True)
            files.append(entry)

        for entry in (*dirs, *files):
            entries.append(entry)
            rows.append(entry.row)
            fingerprint.append(self._entry_signature(entry))

        return _FileBrowserSnapshot(entries, rows, tuple(fingerprint), error)

    def _apply_snapshot(self, snapshot: _FileBrowserSnapshot) -> None:
        self._entries = snapshot.entries
        self._rows = snapshot.rows
        self._entries_signature = snapshot.signature
        self._error = snapshot.error
        self._cache_version = next(self._CACHE_EPOCH)

    def refresh_from_disk(self) -> bool:
        snapshot = self._capture_directory_snapshot()
        signature_changed = snapshot.signature != self._entries_signature
        error_changed = snapshot.error != self._error
        if not signature_changed and not error_changed:
            return False
        self._apply_snapshot(snapshot)
        return True

    def _format_entry(self, path: Path, *, is_dir: bool, openable: bool) -> _FileBrowserEntry:
        try:
            stat_result = path.stat()
        except OSError:
            stat_result = None
        size_display = "" if is_dir else self._format_size(stat_result)
        modified_display = self._format_modified(stat_result)
        name = f"{path.name}/" if is_dir else path.name
        return self._build_entry(
            name=name or path.as_posix(),
            path=path,
            is_dir=is_dir,
            openable=openable,
            size_display=size_display,
            modified_display=modified_display,
        )

    @staticmethod
    def _build_entry(
        *,
        name: str,
        path: Path,
        is_dir: bool,
        openable: bool,
        size_display: str,
        modified_display: str,
    ) -> _FileBrowserEntry:
        row = {
            "name": name,
            "type": "dir" if is_dir else "file",
            "size": size_display,
            "modified": modified_display,
        }
        return _FileBrowserEntry(row=row, path=path, is_dir=is_dir, openable=openable)

    @staticmethod
    def _entry_signature(entry: _FileBrowserEntry) -> tuple[str, str, str, str, bool]:
        row = entry.row
        return (
            str(row.get("name", "")),
            str(row.get("type", "")),
            str(row.get("size", "")),
            str(row.get("modified", "")),
            bool(entry.openable),
        )

    @staticmethod
    def _format_size(stat_result: Any | None) -> str:
        if stat_result is None:
            return "?"
        size = getattr(stat_result, "st_size", None)
        if not isinstance(size, int) or size < 0:
            return "?"
        units = ("B", "KB", "MB", "GB", "TB")
        value = float(size)
        for unit in units:
            if value < 1024 or unit == units[-1]:
                return f"{value:.0f}{unit}" if unit == "B" else f"{value:.1f}{unit}"
            value /= 1024
        return f"{size}B"

    @staticmethod
    def _format_modified(stat_result: Any | None) -> str:
        timestamp = getattr(stat_result, "st_mtime", None)
        if not isinstance(timestamp, (int, float)):
            return ""
        try:
            return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M")
        except (OverflowError, OSError, ValueError):
            return ""

    def fetch_slice(
        self,
        row_start: int,
        row_count: int,
        columns: Sequence[str],
    ) -> TableSlice:
        if row_count <= 0:
            return TableSlice.empty(columns=columns or self.columns, schema=self._schema)

        start = max(row_start, 0)
        end = min(start + row_count, len(self._rows))
        if start >= end:
            return TableSlice.empty(columns=columns or self.columns, schema=self._schema)

        requested = list(columns or self.columns)
        column_data: dict[str, list[Any]] = {name: [] for name in requested}
        for row in self._rows[start:end]:
            for name in requested:
                column_data[name].append(row.get(name, ""))

        table_columns = tuple(
            TableColumn(name, tuple(values), self._schema.get(name), 0)
            for name, values in column_data.items()
        )
        return TableSlice(table_columns, self._schema, start_offset=start)

    def preview(self, rows: int, cols: Sequence[str] | None = None) -> TableSlice:
        return self.fetch_slice(0, rows, cols or self.columns)

    def value_at(self, row: int, col: str) -> Any:
        entry = self._entry_at(row)
        if entry is None:
            return None
        return entry.row.get(col)

    def row_count(self) -> int | None:
        return len(self._rows)

    def supports(self, feature: SheetFeature, /) -> bool:
        return feature in self._CAPABILITIES

    def __len__(self) -> int:
        return len(self._rows)

    def action_for_row(self, row: int) -> FileBrowserAction | None:
        entry = self._entry_at(row)
        if entry is None:
            return None
        if entry.is_dir:
            return FileBrowserAction(type="enter-directory", path=entry.path)
        if entry.openable:
            return FileBrowserAction(type="open-file", path=entry.path)
        return None

    def enter_action(self, viewer: Any) -> SheetEnterAction | None:
        try:
            row = int(getattr(viewer, "cur_row", 0))
        except Exception:
            row = 0
        action = self.action_for_row(row)
        if action is None:
            return None
        if action.type == "enter-directory":
            return SheetEnterAction(kind="open-path", path=action.path, open_as="directory")
        if action.type == "open-file":
            return SheetEnterAction(kind="open-path", path=action.path, open_as="dataset")
        return None

    def is_row_selectable(self, row: int) -> bool:
        entry = self._entry_at(row)
        if entry is None:
            return False
        return entry.row.get("name") != ".."

    def selection_block_reason(self, row: int) -> str | None:
        entry = self._entry_at(row)
        if entry is None:
            return None
        if entry.row.get("name") == "..":
            return "parent directory is not selectable"
        return None

    def at_path(self, directory: Path) -> FileBrowserSheet:
        return FileBrowserSheet(directory, scanners=self._scanners)

    def _entry_at(self, row: int) -> _FileBrowserEntry | None:
        if row < 0 or row >= len(self._entries):
            return None
        return self._entries[row]

    def deletable_entries_for_rows(self, rows: Sequence[int]) -> list[_FileBrowserEntry]:
        seen: set[Path] = set()
        entries: list[_FileBrowserEntry] = []
        for raw_row in rows:
            try:
                row = int(raw_row)
            except Exception:
                continue
            entry = self._entry_at(row)
            if entry is None:
                continue
            if entry.row.get("name") == "..":
                continue
            if entry.path in seen:
                continue
            seen.add(entry.path)
            entries.append(entry)
        return entries

    def entries_for_rows(self, rows: Sequence[int]) -> list[_FileBrowserEntry]:
        seen: set[Path] = set()
        entries: list[_FileBrowserEntry] = []
        for raw_row in rows:
            try:
                row = int(raw_row)
            except Exception:
                continue
            entry = self._entry_at(row)
            if entry is None:
                continue
            if entry.row.get("name") == "..":
                continue
            if entry.path in seen:
                continue
            seen.add(entry.path)
            entries.append(entry)
        return entries

    def _count_files(
        self, entries: Sequence[_FileBrowserEntry]
    ) -> tuple[int, list[tuple[Path, str]]]:
        files = 0
        errors: list[tuple[Path, str]] = []
        seen: set[Path] = set()
        for entry in entries:
            path = entry.path
            if path in seen:
                continue
            seen.add(path)
            if entry.is_dir:
                try:
                    for _root, _dirs, filenames in os.walk(path):
                        files += len(filenames)
                except OSError as exc:
                    errors.append((path, f"count error: {exc}"))
            else:
                files += 1
        return files, errors

    def deletion_impact(
        self, entries: Sequence[_FileBrowserEntry]
    ) -> tuple[int, list[tuple[Path, str]]]:
        """Return the number of files that would be deleted (recursively) for ``entries``."""

        return self._count_files(entries)

    def delete_entries(self, entries: Sequence[_FileBrowserEntry]) -> FileDeletionResult:
        deleted: list[Path] = []
        errors: list[tuple[Path, str]] = []
        seen: set[Path] = set()
        for entry in entries:
            path = entry.path
            if path in seen:
                continue
            seen.add(path)
            if entry.is_dir:
                try:
                    shutil.rmtree(path)
                except FileNotFoundError:
                    deleted.append(path)
                except OSError as exc:  # pragma: no cover - filesystem specific
                    errors.append((path, str(exc)))
                    continue
                else:
                    deleted.append(path)
                continue
            try:
                path.unlink(missing_ok=True)
            except IsADirectoryError:
                try:
                    shutil.rmtree(path)
                except OSError as exc:  # pragma: no cover - filesystem specific
                    errors.append((path, str(exc)))
                    continue
                deleted.append(path)
            except OSError as exc:  # pragma: no cover - filesystem specific
                errors.append((path, str(exc)))
                continue
            else:
                deleted.append(path)

        if deleted or errors:
            snapshot = self._capture_directory_snapshot()
            self._apply_snapshot(snapshot)

        return FileDeletionResult(tuple(deleted), tuple(errors))

    # Layout hints ----------------------------------------------------

    @property
    def compact_width_layout(self) -> bool:
        return False

    @property
    def preferred_fill_column(self) -> str:
        return "name"


def file_browser_status_text(sheet: FileBrowserSheet) -> str:
    message = getattr(sheet, "status_message", None)
    if message:
        return message
    count = sheet.row_count() or 0
    return f"{count} entries"


__all__ = [
    "FileBrowserAction",
    "FileBrowserSheet",
    "FileDeletionResult",
    "file_browser_status_text",
]
