"""SDK thin client for server-side file text extraction.

The SDK has zero parsing dependencies — all text extraction happens server-side.
This module only normalizes input types and sends bytes to the server via
multipart/form-data.
"""

from __future__ import annotations

from pathlib import Path
from typing import BinaryIO

import requests as _requests

from .exceptions import FileParseError, PasswordProtectedError, UnsupportedFormatError

SUPPORTED_FORMATS = ("pdf", "txt", "csv", "json", "docx")
DEFAULT_MAX_SIZE_MB = 10


class FileParser:
    """Thin client for server-side file text extraction.

    Accepts file input (bytes, str path, Path, or BinaryIO), sends it to
    the security server's /api/security/parse-file endpoint via multipart/form-data,
    and returns the extracted text as a plain str.

    The SDK has zero parsing dependencies — all text extraction happens server-side.

    Args:
        base_url: Security server base URL (e.g., "https://app.meshulash.ai").
        session: An authenticated requests.Session (from Guard._session).
        max_size_mb: Maximum file size in MB. Default 10.
        timeout: Request timeout in seconds. Default 60.
    """

    def __init__(
        self,
        base_url: str,
        session: _requests.Session,
        max_size_mb: float = DEFAULT_MAX_SIZE_MB,
        timeout: int = 60,
    ) -> None:
        self._base_url = base_url
        self._session = session
        self._max_size_mb = max_size_mb
        self._timeout = timeout

    def parse(
        self,
        file: bytes | str | Path | BinaryIO,
        format: str | None = None,
    ) -> str:
        """Extract text from a file. Returns plain str.

        Normalizes input (bytes, path, or file-like object) to bytes, enforces
        the configured size limit, then POSTs to /api/security/parse-file via
        multipart/form-data. All parsing happens server-side.

        Args:
            file: File content as bytes, path (str or Path), or file-like object
                (BinaryIO). For bytes input, ``format`` must be provided explicitly
                because there is no filename from which to detect the extension.
            format: Format hint ("pdf", "txt", "csv", "json", "docx"). Auto-detected
                from file extension if None. Must be provided when passing raw bytes.

        Returns:
            Extracted text as a plain str.

        Raises:
            FileParseError: File is corrupted, unreadable, or exceeds size limit.
            PasswordProtectedError: PDF or DOCX is password-protected.
            UnsupportedFormatError: File format is not supported or cannot be
                determined.
        """
        file_bytes, detected_format = self._normalize_input(file, format)

        max_bytes = int(self._max_size_mb * 1024 * 1024)
        if len(file_bytes) > max_bytes:
            raise FileParseError(
                f"File size {len(file_bytes) / 1024 / 1024:.1f} MB exceeds "
                f"limit of {self._max_size_mb} MB"
            )

        url = f"{self._base_url}/api/security/parse-file"
        resp = self._session.post(
            url,
            files={"file": ("upload." + detected_format, file_bytes, "application/octet-stream")},
            data={"format": detected_format},
            timeout=self._timeout,
        )

        if resp.status_code == 400:
            error = resp.json().get("error", "")
            if "password" in error.lower():
                raise PasswordProtectedError(error)
            if "unsupported" in error.lower():
                raise UnsupportedFormatError(error)
            raise FileParseError(error)

        if resp.status_code == 422:
            raise FileParseError(f"File could not be parsed: {resp.text}")

        if resp.status_code >= 500:
            from .exceptions import ServerError
            raise ServerError(f"Server error {resp.status_code}", resp.status_code)

        resp.raise_for_status()
        return resp.json()["text"]

    def _normalize_input(
        self,
        file: bytes | str | Path | BinaryIO,
        format_hint: str | None,
    ) -> tuple[bytes, str]:
        """Normalize all input types to (bytes, format_string).

        Reads file bytes exactly once (critical for BinaryIO — the read cursor
        is exhausted after the first read). Determines format from the format
        hint or the file extension, whichever is available.

        Args:
            file: Raw input — bytes, str path, Path, or BinaryIO.
            format_hint: Explicit format override from caller, or None.

        Returns:
            Tuple of (file_bytes, format_string).

        Raises:
            UnsupportedFormatError: Format cannot be determined or is not supported.
        """
        if isinstance(file, (str, Path)):
            path = Path(file)
            ext = path.suffix.lstrip(".").lower()
            file_bytes = path.read_bytes()
        elif isinstance(file, bytes):
            ext = ""
            file_bytes = file
        else:
            # BinaryIO / file-like object — call read() exactly once
            name = getattr(file, "name", "") or ""
            ext = Path(name).suffix.lstrip(".").lower() if name else ""
            file_bytes = file.read()

        fmt = format_hint or ext
        if not fmt:
            raise UnsupportedFormatError(
                "Cannot determine file format. Pass format= explicitly. "
                f"Supported: {', '.join(SUPPORTED_FORMATS)}"
            )

        fmt = fmt.lower()
        if fmt not in SUPPORTED_FORMATS:
            raise UnsupportedFormatError(
                f"Unsupported format: {fmt!r}. "
                f"Supported formats: {', '.join(SUPPORTED_FORMATS)}"
            )

        return file_bytes, fmt
