"""Synology FileStation backed file sharing."""

from __future__ import annotations

from dataclasses import dataclass
import json
import mimetypes
import posixpath
from pathlib import Path
import ssl
from typing import Any, Callable
from urllib import parse
from urllib import request
from urllib.error import HTTPError
import uuid

from a2a_agent.config import ShareConfig


DEFAULT_TIMEOUT_SECONDS = 30.0
FILESTATION_SESSION = "FileStation"


class ShareError(RuntimeError):
    """Raised when the shared file store cannot complete an operation."""


class ShareAuthenticationError(ShareError):
    """Raised when FileStation login fails."""


class ShareApiError(ShareError):
    """Raised when FileStation returns a JSON API error."""


@dataclass(frozen=True)
class HttpFile:
    field_name: str
    filename: str
    content: bytes
    content_type: str = "application/octet-stream"


@dataclass(frozen=True)
class HttpRequest:
    method: str
    url: str
    params: dict[str, Any]
    fields: dict[str, Any] | None = None
    files: tuple[HttpFile, ...] = ()
    timeout: float = DEFAULT_TIMEOUT_SECONDS
    insecure: bool = False


@dataclass(frozen=True)
class HttpResponse:
    status: int
    body: bytes


Transport = Callable[[HttpRequest], HttpResponse]


class FileStationClient:
    def __init__(
        self,
        config: ShareConfig,
        *,
        transport: Transport | None = None,
        timeout: float = DEFAULT_TIMEOUT_SECONDS,
    ) -> None:
        self.config = config
        self._transport = transport or urllib_transport
        self._timeout = timeout
        self._sid: str | None = None

    @property
    def sid(self) -> str | None:
        return self._sid

    def login(self) -> str:
        try:
            payload = self._json_request(
                "auth.cgi",
                {
                    "api": "SYNO.API.Auth",
                    "version": "3",
                    "method": "login",
                    "account": self.config.user,
                    "passwd": self.config.password,
                    "session": FILESTATION_SESSION,
                    "format": "sid",
                },
            )
        except ShareApiError as exc:
            raise ShareAuthenticationError(f"FileStation login failed: {exc}") from exc
        sid = payload.get("data", {}).get("sid")
        if not sid:
            raise ShareAuthenticationError("FileStation login did not return a session id")
        self._sid = str(sid)
        return self._sid

    def upload(self, local_path: str | Path, remote_dir: str) -> str:
        self._ensure_sid()
        source = Path(local_path)
        if not source.is_file():
            raise ShareError(f"upload source is not a file: {source}")

        remote_directory = self.absolute_dir(remote_dir)
        content = source.read_bytes()
        content_type = mimetypes.guess_type(source.name)[0] or "application/octet-stream"
        # FileStation Upload는 api/version/method를 멀티파트 폼 본문에 넣어야 한다.
        # (URL 쿼리에 넣으면 오류 119) _sid만 쿼리로 전달.
        payload = self._json_request(
            "entry.cgi",
            {
                "_sid": self._sid,
            },
            method="POST",
            fields={
                "api": "SYNO.FileStation.Upload",
                "version": "2",
                "method": "upload",
                "path": remote_directory,
                "create_parents": "true",
                "overwrite": "true",
            },
            files=(
                HttpFile(
                    field_name="file",
                    filename=source.name,
                    content=content,
                    content_type=content_type,
                ),
            ),
        )
        if payload.get("success") is not True:
            raise ShareApiError("FileStation upload failed")
        return posixpath.join(remote_directory, source.name)

    def download(self, remote_path: str, local_path: str | Path) -> Path:
        self._ensure_sid()
        destination = Path(local_path)
        destination.parent.mkdir(parents=True, exist_ok=True)
        absolute_remote_path = self.absolute_path(remote_path)
        response = self._request(
            "entry.cgi",
            {
                "api": "SYNO.FileStation.Download",
                "version": "2",
                "method": "download",
                "path": absolute_remote_path,
                "mode": "download",
                "_sid": self._sid,
            },
        )
        if response.status < 200 or response.status >= 300:
            raise ShareApiError(f"FileStation download failed: HTTP {response.status}")
        destination.write_bytes(response.body)
        return destination

    def delete(self, remote_path: str) -> None:
        self._ensure_sid()
        self._json_request(
            "entry.cgi",
            {
                "api": "SYNO.FileStation.Delete",
                "version": "2",
                "method": "delete",
                "path": self.absolute_path(remote_path),
                "_sid": self._sid,
            },
            method="POST",
        )

    def logout(self) -> None:
        if not self._sid:
            return
        try:
            self._json_request(
                "auth.cgi",
                {
                    "api": "SYNO.API.Auth",
                    "version": "3",
                    "method": "logout",
                    "session": FILESTATION_SESSION,
                    "_sid": self._sid,
                },
            )
        finally:
            self._sid = None

    def absolute_dir(self, remote_dir: str) -> str:
        remote = self.absolute_path(remote_dir)
        return remote.rstrip("/") or self.config.folder

    def absolute_path(self, remote_path: str) -> str:
        if not remote_path:
            return self.config.folder
        if remote_path.startswith("/"):
            normalized = posixpath.normpath(remote_path)
        else:
            normalized = posixpath.normpath(posixpath.join(self.config.folder, remote_path))
        self._ensure_inside_share(normalized)
        return normalized

    def relative_path(self, remote_path: str) -> str:
        absolute = self.absolute_path(remote_path)
        if absolute == self.config.folder:
            return ""
        prefix = self.config.folder.rstrip("/") + "/"
        return absolute.removeprefix(prefix)

    def _ensure_sid(self) -> None:
        if not self._sid:
            self.login()

    def _json_request(
        self,
        api_path: str,
        params: dict[str, Any],
        *,
        method: str = "GET",
        fields: dict[str, Any] | None = None,
        files: tuple[HttpFile, ...] = (),
    ) -> dict[str, Any]:
        response = self._request(api_path, params, method=method, fields=fields, files=files)
        try:
            payload = json.loads(response.body.decode("utf-8"))
        except json.JSONDecodeError as exc:
            raise ShareApiError("FileStation returned invalid JSON") from exc

        if response.status < 200 or response.status >= 300:
            raise ShareApiError(f"FileStation returned HTTP {response.status}")
        if payload.get("success") is False:
            code = payload.get("error", {}).get("code", "unknown")
            raise ShareApiError(f"FileStation API error code {code}")
        return payload

    def _request(
        self,
        api_path: str,
        params: dict[str, Any],
        *,
        method: str = "GET",
        fields: dict[str, Any] | None = None,
        files: tuple[HttpFile, ...] = (),
    ) -> HttpResponse:
        http_request = HttpRequest(
            method=method,
            url=self._url(api_path),
            params=params,
            fields=fields,
            files=files,
            timeout=self._timeout,
            insecure=self.config.insecure,
        )
        try:
            return self._transport(http_request)
        except ShareError:
            raise
        except Exception as exc:
            raise ShareError(f"FileStation request failed: {exc}") from exc

    def _url(self, api_path: str) -> str:
        host = self.config.host.rstrip("/")
        if host.startswith(("http://", "https://")):
            base = host
        else:
            base = f"https://{host}:{self.config.port}"
        return f"{base}/webapi/{api_path.lstrip('/')}"

    def _ensure_inside_share(self, remote_path: str) -> None:
        folder = self.config.folder.rstrip("/")
        if remote_path == folder or remote_path.startswith(folder + "/"):
            return
        raise ShareError(f"remote path is outside configured share folder: {remote_path}")


def urllib_transport(http_request: HttpRequest) -> HttpResponse:
    method = http_request.method.upper()
    params = {key: str(value) for key, value in http_request.params.items()}
    form_fields = {key: str(value) for key, value in (http_request.fields or {}).items()}
    headers: dict[str, str] = {}
    data: bytes | None = None
    url = http_request.url

    if http_request.files:
        # 멀티파트 업로드: params(_sid 등)는 URL 쿼리에, form 필드와 파일만 본문에.
        # (FileStation Upload는 _sid를 본문에 넣으면 세션을 못 찾는다)
        url = url_with_query(url, params)
        data, content_type = encode_multipart(form_fields, http_request.files)
        headers["Content-Type"] = content_type
        headers["Content-Length"] = str(len(data))
    elif method == "POST":
        data = parse.urlencode({**params, **form_fields}).encode("utf-8")
        headers["Content-Type"] = "application/x-www-form-urlencoded"
    else:
        url = url_with_query(url, {**params, **form_fields})

    req = request.Request(url, data=data, headers=headers, method=method)
    context = ssl._create_unverified_context() if http_request.insecure else None
    try:
        with request.urlopen(req, timeout=http_request.timeout, context=context) as response:
            return HttpResponse(status=response.status, body=response.read())
    except HTTPError as exc:
        return HttpResponse(status=exc.code, body=exc.read())


def url_with_query(url: str, params: dict[str, Any]) -> str:
    if not params:
        return url
    separator = "&" if parse.urlparse(url).query else "?"
    return url + separator + parse.urlencode(params)


def encode_multipart(
    fields: dict[str, str],
    files: tuple[HttpFile, ...],
) -> tuple[bytes, str]:
    boundary = f"a2a-{uuid.uuid4().hex}"
    chunks: list[bytes] = []
    for name, value in fields.items():
        chunks.extend(
            [
                f"--{boundary}\r\n".encode("utf-8"),
                f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode("utf-8"),
                value.encode("utf-8"),
                b"\r\n",
            ]
        )
    for file in files:
        chunks.extend(
            [
                f"--{boundary}\r\n".encode("utf-8"),
                (
                    "Content-Disposition: form-data; "
                    f'name="{file.field_name}"; filename="{file.filename}"\r\n'
                ).encode("utf-8"),
                f"Content-Type: {file.content_type}\r\n\r\n".encode("utf-8"),
                file.content,
                b"\r\n",
            ]
        )
    chunks.append(f"--{boundary}--\r\n".encode("utf-8"))
    return b"".join(chunks), f"multipart/form-data; boundary={boundary}"
