"""Remote directory listing — shared by POST /api/sync/browse and GET /api/files/browse."""

from __future__ import annotations

import hashlib
import logging
import shlex
import time
from typing import Any

from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession

from server.infrastructure.database.server_repo import ServerRepositoryImpl
from server.infrastructure.ssh.remote_shell import exec_ssh_command_with_fallback
from server.utils.posix_paths import PosixPathError, normalize_remote_abs_path
from server.utils.unix_ls import parse_ls_la_line, perms_to_mode_octal

logger = logging.getLogger(__name__)

BROWSE_ETAG_CACHE_TTL_SEC = 5.0
BROWSE_ETAG_CACHE_MAX = 200
_BROWSE_ETAG_CACHE: dict[str, tuple[str, dict[str, Any], float]] = {}


def browse_cache_key(server_id: int, path: str) -> str:
    return f"{server_id}:{path}"


def compute_browse_etag(path: str, items: list[dict[str, Any]]) -> str:
    """Weak ETag from path + sorted entry fingerprints (name|mtime|size)."""
    lines = [path]
    for item in sorted(items, key=lambda x: x.get("name") or ""):
        lines.append(f"{item.get('name', '')}|{item.get('modified', '')}|{item.get('size', 0)}")
    digest = hashlib.sha256("\n".join(lines).encode("utf-8")).hexdigest()
    return f'W/"{digest}"'


def _prune_browse_etag_cache() -> None:
    if len(_BROWSE_ETAG_CACHE) <= BROWSE_ETAG_CACHE_MAX:
        return
    oldest_key = min(_BROWSE_ETAG_CACHE, key=lambda k: _BROWSE_ETAG_CACHE[k][2])
    _BROWSE_ETAG_CACHE.pop(oldest_key, None)


def get_cached_browse(server_id: int, path: str) -> tuple[str, dict[str, Any]] | None:
    """Return (etag, payload) if server-side ETag cache is still fresh."""
    key = browse_cache_key(server_id, path)
    entry = _BROWSE_ETAG_CACHE.get(key)
    if not entry:
        return None
    etag, payload, at = entry
    if time.monotonic() - at > BROWSE_ETAG_CACHE_TTL_SEC:
        _BROWSE_ETAG_CACHE.pop(key, None)
        return None
    return etag, payload


def set_cached_browse(server_id: int, path: str, etag: str, payload: dict[str, Any]) -> None:
    key = browse_cache_key(server_id, path)
    _BROWSE_ETAG_CACHE[key] = (etag, payload, time.monotonic())
    _prune_browse_etag_cache()


def invalidate_browse_parents(server_id: int, *paths: str) -> None:
    """Drop ETag cache for parent directories of one or more remote paths."""
    from server.utils.posix_paths import PosixPathError, normalize_remote_abs_path, posix_dirname

    seen: set[str] = set()
    for raw in paths:
        if not raw:
            continue
        try:
            parent = posix_dirname(normalize_remote_abs_path(raw))
        except PosixPathError:
            continue
        if parent in seen:
            continue
        seen.add(parent)
        invalidate_browse_cache(server_id, parent)


def invalidate_browse_cache(server_id: int | None = None, path: str | None = None) -> None:
    """Drop server ETag cache after mutations or when forcing refresh."""
    if server_id is None:
        _BROWSE_ETAG_CACHE.clear()
        return
    if path is None:
        prefix = f"{server_id}:"
        for key in list(_BROWSE_ETAG_CACHE):
            if key.startswith(prefix):
                _BROWSE_ETAG_CACHE.pop(key, None)
        return
    _BROWSE_ETAG_CACHE.pop(browse_cache_key(server_id, path), None)


async def list_remote_directory(
    session: AsyncSession,
    server_id: int,
    path: str,
) -> dict[str, Any]:
    """SSH ls -la on remote path; same JSON shape as legacy POST /sync/browse."""
    if not server_id:
        raise HTTPException(status_code=400, detail="server_id required")

    repo = ServerRepositoryImpl(session)
    server = await repo.get_by_id(server_id)
    if not server:
        raise HTTPException(status_code=404, detail="Server not found")

    try:
        path = normalize_remote_abs_path(path)
    except PosixPathError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc

    path_q = shlex.quote(path)
    result = await exec_ssh_command_with_fallback(
        server,
        f"ls -la --time-style=long-iso {path_q}",
        timeout=10,
    )
    if result["exit_code"] != 0:
        result = await exec_ssh_command_with_fallback(
            server, f"ls -la {path_q}", timeout=10
        )

    if result["exit_code"] != 0:
        return {"path": path, "items": [], "error": (result.get("stderr") or "")[:500]}

    raw_entries = []
    for line in result["stdout"].split("\n"):
        parsed = parse_ls_la_line(line)
        if parsed:
            raw_entries.append(parsed)

    items: list[dict[str, Any]] = []
    for entry in raw_entries:
        is_dir = bool(entry["is_dir"])
        is_link = bool(entry["is_link"])
        try:
            size = int(entry["size"]) if not is_dir and not is_link else 0
        except (TypeError, ValueError):
            size = 0
        if is_dir:
            ftype = "directory"
        elif is_link:
            ftype = "symlink"
        else:
            ftype = "file"
        mode_octal = perms_to_mode_octal(entry["perms"]) or ""
        items.append({
            "name": entry["name"],
            "type": ftype,
            "size": size,
            "modified": entry["modified"],
            "permissions": entry["perms"],
            "owner": entry.get("owner") or "",
            "group": entry.get("group") or "",
            "mode_octal": mode_octal,
            "link_target": entry.get("link_target"),
        })

    return {"path": path, "items": items}


def etag_for_payload(payload: dict[str, Any]) -> str:
    return compute_browse_etag(str(payload.get("path") or "/"), list(payload.get("items") or []))
