from __future__ import annotations

from dataclasses import dataclass
from pathlib import PurePosixPath
from urllib.parse import urlencode

import httpx

from backend.app.api.errors import AppError
from backend.app.api.schemas import FileInfoResponse, RemoteClientItem, ViewResponse
from backend.app.services.remote_browse_service import RemoteBrowseService
from backend.app.services.remote_client_service import RemoteClientService

REMOTE_TEXT_PREVIEW_MAX_BYTES = 256 * 1024
REMOTE_AGENT_TIMEOUT_SECONDS = 2.0
REMOTE_DOWNLOAD_READ_TIMEOUT_SECONDS = 5.0
REMOTE_STREAM_CHUNK_BYTES = 64 * 1024
TEXT_CONTENT_TYPES = {
    ".txt": "text/plain",
    ".log": "text/plain",
    ".conf": "text/plain",
    ".ini": "text/plain",
    ".cfg": "text/plain",
    ".md": "text/markdown",
    ".yml": "text/yaml",
    ".yaml": "text/yaml",
    ".json": "application/json",
    ".js": "text/javascript",
    ".py": "text/x-python",
    ".css": "text/css",
    ".html": "text/html",
}
SPECIAL_TEXT_FILENAMES = {
    "dockerfile": "text/plain",
    "containerfile": "text/plain",
}
IMAGE_CONTENT_TYPES = {
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".png": "image/png",
    ".webp": "image/webp",
    ".gif": "image/gif",
    ".bmp": "image/bmp",
    ".avif": "image/avif",
}


@dataclass(frozen=True)
class RemoteResolvedPath:
    raw_path: str
    client: RemoteClientItem
    share_key: str
    relative_path: str
    name: str
    root_path: str


class RemoteFileService:
    def __init__(
        self,
        remote_client_service: RemoteClientService,
        agent_auth_header: str,
        agent_auth_scheme: str,
        agent_auth_token: str,
        agent_timeout_seconds: float = REMOTE_AGENT_TIMEOUT_SECONDS,
        text_preview_max_bytes: int = REMOTE_TEXT_PREVIEW_MAX_BYTES,
        download_read_timeout_seconds: float = REMOTE_DOWNLOAD_READ_TIMEOUT_SECONDS,
        stream_chunk_bytes: int = REMOTE_STREAM_CHUNK_BYTES,
    ):
        self._remote_client_service = remote_client_service
        self._agent_auth_header = (agent_auth_header or "Authorization").strip() or "Authorization"
        self._agent_auth_scheme = (agent_auth_scheme or "Bearer").strip() or "Bearer"
        self._agent_auth_token = (agent_auth_token or "").strip()
        self._agent_timeout_seconds = max(0.1, float(agent_timeout_seconds))
        self._text_preview_max_bytes = max(1024, int(text_preview_max_bytes))
        self._download_read_timeout_seconds = max(0.1, float(download_read_timeout_seconds))
        self._stream_chunk_bytes = max(4096, int(stream_chunk_bytes))

    def handles_path(self, path: str) -> bool:
        return RemoteBrowseService.handles_path(path)

    def info(self, path: str) -> FileInfoResponse:
        resolved = self._resolve_remote_path(path, allow_share_root=True)
        payload = self._request_json(
            client=resolved.client,
            endpoint_path="/api/info",
            params={"share": resolved.share_key, "path": resolved.relative_path},
        )
        kind = str(payload.get("kind", "")).strip()
        if kind not in {"file", "directory"}:
            raise self._invalid_agent_payload(resolved.client, "Remote file info response was invalid")

        extension = str(payload.get("extension", "") or "").strip() or PurePosixPath(resolved.name).suffix.lower() or None
        return FileInfoResponse(
            name=str(payload.get("name", resolved.name)).strip() or resolved.name,
            path=resolved.raw_path,
            type=kind,
            size=self._normalize_optional_int(payload.get("size")),
            modified=str(payload.get("modified", "")).strip(),
            root=resolved.root_path,
            extension=extension,
            content_type=self._normalize_optional_string(payload.get("content_type")),
            owner=self._normalize_optional_string(payload.get("owner")),
            group=self._normalize_optional_string(payload.get("group")),
            width=self._normalize_optional_int(payload.get("width")),
            height=self._normalize_optional_int(payload.get("height")),
        )

    def view(self, path: str, *, for_edit: bool = False) -> ViewResponse:
        if for_edit:
            raise AppError(
                code="unsupported_type",
                message="Remote files are not supported for edit",
                status_code=409,
                details={"path": path},
            )
        resolved = self._resolve_remote_path(path)
        payload = self._request_json(
            client=resolved.client,
            endpoint_path="/api/read",
            params={
                "share": resolved.share_key,
                "path": resolved.relative_path,
                "max_bytes": str(self._text_preview_max_bytes),
            },
        )
        content = str(payload.get("content", ""))
        if len(content.encode("utf-8")) > self._text_preview_max_bytes:
            raise self._invalid_agent_payload(resolved.client, "Remote text preview exceeded the configured limit")
        return ViewResponse(
            path=resolved.raw_path,
            name=str(payload.get("name", resolved.name)).strip() or resolved.name,
            content_type=str(payload.get("content_type", self._content_type_for_name(resolved.name) or "text/plain")).strip(),
            encoding=str(payload.get("encoding", "utf-8")).strip() or "utf-8",
            truncated=bool(payload.get("truncated", False)),
            size=max(0, int(payload.get("size", 0))),
            modified=str(payload.get("modified", "")).strip(),
            content=content,
        )

    def prepare_download(self, paths: list[str]) -> dict:
        if len(paths) != 1:
            raise AppError(
                code="invalid_request",
                message="Remote downloads support exactly one file per request",
                status_code=400,
            )
        resolved = self._resolve_remote_path(paths[0])
        stream = self._open_stream(
            client=resolved.client,
            endpoint_path="/api/download",
            params={"share": resolved.share_key, "path": resolved.relative_path},
        )
        content_disposition = stream.headers.get("content-disposition") or f'attachment; filename="{resolved.name}"'
        headers = {"Content-Disposition": content_disposition}
        if stream.headers.get("content-length"):
            headers["Content-Length"] = stream.headers["content-length"]
        return {
            "content": self._iter_remote_stream(stream),
            "headers": headers,
            "content_type": stream.headers.get("content-type", "application/octet-stream"),
        }

    def prepare_image_stream(self, path: str) -> dict:
        resolved = self._resolve_remote_path(path)
        content_type = self._image_content_type_for_name(resolved.name)
        if content_type is None:
            raise AppError(
                code="unsupported_type",
                message="File type is not supported for image viewing",
                status_code=409,
                details={"path": path},
            )
        stream = self._open_stream(
            client=resolved.client,
            endpoint_path="/api/download",
            params={"share": resolved.share_key, "path": resolved.relative_path},
        )
        headers: dict[str, str] = {}
        if stream.headers.get("content-length"):
            headers["Content-Length"] = stream.headers["content-length"]
        return {
            "content": self._iter_remote_stream(stream),
            "headers": headers,
            "content_type": content_type,
        }

    def _resolve_remote_path(self, path: str, *, allow_share_root: bool = False) -> RemoteResolvedPath:
        normalized = (path or "").strip().rstrip("/")
        if not self.handles_path(normalized):
            raise AppError(
                code="invalid_request",
                message="Remote path must be under /Clients",
                status_code=400,
                details={"path": path},
            )
        parts = normalized[len(RemoteBrowseService.ROOT_PATH) + 1 :].split("/") if normalized != RemoteBrowseService.ROOT_PATH else []
        min_parts = 2 if allow_share_root else 3
        if len(parts) < min_parts:
            raise AppError(
                code="type_conflict",
                message="Remote path must reference a file or directory inside a share",
                status_code=409,
                details={"path": path},
            )
        client = self._remote_client_service.get_client(parts[0])
        if client.status != "online":
            raise AppError(
                code="remote_client_unavailable",
                message=f"Remote client '{client.display_name}' is offline",
                status_code=503,
                details={"client_id": client.client_id, "status": client.status},
            )
        share_key = parts[1]
        if not any(share.key == share_key for share in client.shares):
            raise AppError(
                code="path_not_found",
                message="Remote share was not found",
                status_code=404,
                details={"client_id": client.client_id, "share_key": share_key},
            )
        relative_path = "/".join(parts[2:])
        if not relative_path and not allow_share_root:
            raise AppError(
                code="type_conflict",
                message="Remote file operation requires a path inside the share",
                status_code=409,
                details={"path": path},
            )
        name = parts[-1]
        if allow_share_root and len(parts) == 2:
            share = next((item for item in client.shares if item.key == share_key), None)
            if share is not None:
                name = share.label
        return RemoteResolvedPath(
            raw_path=normalized,
            client=client,
            share_key=share_key,
            relative_path=relative_path,
            name=name,
            root_path=f"{RemoteBrowseService.ROOT_PATH}/{client.client_id}/{share_key}",
        )

    def _request_json(self, *, client: RemoteClientItem, endpoint_path: str, params: dict[str, str]) -> dict:
        url = self._build_url(client.endpoint, endpoint_path, params)
        timeout = httpx.Timeout(self._agent_timeout_seconds, connect=self._agent_timeout_seconds)
        try:
            with httpx.Client(timeout=timeout, headers=self._auth_headers()) as client_http:
                response = client_http.get(url)
        except httpx.TimeoutException as exc:
            raise self._timeout_error(client) from exc
        except httpx.HTTPError as exc:
            raise self._unreachable_error(client) from exc
        self._raise_for_agent_error(client=client, response=response)
        try:
            payload = response.json()
        except ValueError as exc:
            raise self._invalid_agent_payload(client, "Remote client returned invalid JSON") from exc
        if not isinstance(payload, dict):
            raise self._invalid_agent_payload(client, "Remote client returned an invalid response")
        return payload

    def _open_stream(self, *, client: RemoteClientItem, endpoint_path: str, params: dict[str, str]) -> httpx.Response:
        url = self._build_url(client.endpoint, endpoint_path, params)
        timeout = httpx.Timeout(
            connect=self._agent_timeout_seconds,
            read=self._download_read_timeout_seconds,
            write=self._agent_timeout_seconds,
            pool=self._agent_timeout_seconds,
        )
        client_http = httpx.Client(timeout=timeout, headers=self._auth_headers())
        try:
            response = client_http.stream("GET", url)
            response.__enter__()
        except httpx.TimeoutException as exc:
            client_http.close()
            raise self._timeout_error(client) from exc
        except httpx.HTTPError as exc:
            client_http.close()
            raise self._unreachable_error(client) from exc
        try:
            self._raise_for_agent_error(client=client, response=response)
        except Exception:
            response.close()
            client_http.close()
            raise
        response.extensions["remote_client_http_client"] = client_http
        return response

    def _iter_remote_stream(self, response: httpx.Response):
        client_http = response.extensions.get("remote_client_http_client")
        try:
            for chunk in response.iter_bytes(chunk_size=self._stream_chunk_bytes):
                if chunk:
                    yield chunk
        finally:
            response.close()
            if client_http is not None:
                client_http.close()

    def _raise_for_agent_error(self, *, client: RemoteClientItem, response: httpx.Response) -> None:
        if response.status_code < 400:
            return
        code = None
        message = None
        detail_payload = None
        try:
            payload = response.json()
        except ValueError:
            payload = None
        if isinstance(payload, dict):
            detail = payload.get("detail")
            if isinstance(detail, dict):
                detail_payload = detail
                code = self._normalize_optional_string(detail.get("code"))
                message = self._normalize_optional_string(detail.get("message"))
            elif isinstance(detail, str):
                message = detail.strip() or None

        if response.status_code == 400:
            raise AppError(
                code=code or "invalid_request",
                message=message or "Remote request was rejected",
                status_code=400,
                details={"client_id": client.client_id},
            )
        if response.status_code == 403:
            agent_code = code or "forbidden"
            if agent_code == "invalid_agent_token":
                raise AppError(
                    code="remote_client_forbidden",
                    message=f"Remote client '{client.display_name}' rejected authentication",
                    status_code=502,
                    details={"client_id": client.client_id, "endpoint": client.endpoint},
                )
            raise AppError(
                code=agent_code,
                message=message or "Remote access was denied",
                status_code=403,
                details={"client_id": client.client_id},
            )
        if response.status_code == 404:
            raise AppError(
                code=code or "path_not_found",
                message=message or "Remote path was not found",
                status_code=404,
                details={"client_id": client.client_id},
            )
        if response.status_code == 409:
            raise AppError(
                code=code or "type_conflict",
                message=message or "Remote file operation could not be completed",
                status_code=409,
                details={"client_id": client.client_id},
            )
        raise AppError(
            code="remote_client_error",
            message=message or f"Remote client '{client.display_name}' request failed",
            status_code=502,
            details={
                "client_id": client.client_id,
                "endpoint": client.endpoint,
                "status_code": str(response.status_code),
                "agent_code": code or "",
                "agent_detail": str(detail_payload or ""),
            },
        )

    def _auth_headers(self) -> dict[str, str]:
        if not self._agent_auth_token:
            raise AppError(
                code="remote_client_agent_auth_not_configured",
                message="Remote client agent auth token is not configured",
                status_code=503,
            )
        return {self._agent_auth_header: f"{self._agent_auth_scheme} {self._agent_auth_token}"}

    @staticmethod
    def _build_url(endpoint: str, endpoint_path: str, params: dict[str, str]) -> str:
        return f"{endpoint.rstrip('/')}{endpoint_path}?{urlencode(params)}"

    @staticmethod
    def _timeout_error(client: RemoteClientItem) -> AppError:
        return AppError(
            code="remote_client_timeout",
            message=f"Remote client '{client.display_name}' timed out",
            status_code=504,
            details={"client_id": client.client_id, "endpoint": client.endpoint},
        )

    @staticmethod
    def _unreachable_error(client: RemoteClientItem) -> AppError:
        return AppError(
            code="remote_client_unreachable",
            message=f"Remote client '{client.display_name}' is unreachable",
            status_code=502,
            details={"client_id": client.client_id, "endpoint": client.endpoint},
        )

    @staticmethod
    def _invalid_agent_payload(client: RemoteClientItem, message: str) -> AppError:
        return AppError(
            code="remote_client_error",
            message=message,
            status_code=502,
            details={"client_id": client.client_id, "endpoint": client.endpoint},
        )

    @staticmethod
    def _normalize_optional_string(value) -> str | None:
        normalized = str(value).strip() if value is not None else ""
        return normalized or None

    @staticmethod
    def _normalize_optional_int(value) -> int | None:
        if value is None or value == "":
            return None
        try:
            return max(0, int(value))
        except (TypeError, ValueError):
            return None

    @staticmethod
    def _content_type_for_name(name: str) -> str | None:
        special_name = SPECIAL_TEXT_FILENAMES.get((name or "").lower())
        if special_name:
            return special_name
        return TEXT_CONTENT_TYPES.get(PurePosixPath(name).suffix.lower())

    @staticmethod
    def _image_content_type_for_name(name: str) -> str | None:
        return IMAGE_CONTENT_TYPES.get(PurePosixPath(name).suffix.lower())
