"""Gravi-Vision Python client."""

import secrets
from datetime import datetime
from pathlib import Path
from typing import BinaryIO, Callable, Optional

import httpx
from pydantic import BaseModel, Field


class VisionResponse(BaseModel):
    """Vision API response."""

    extracted_data: dict[str, str | int | float | list]
    confidence: float | None = None


class EBolDetailRequest(BaseModel):
    """Detail line item for a BOL."""

    product: str
    load_number: str | None = None
    gross_volume: float = Field(gt=0)
    net_volume: float = Field(gt=0)


class EBolRequest(BaseModel):
    """Request model for creating an electronic BOL."""

    source_system: str
    bol_number: str | None = None
    date: datetime | None = None
    terminal: str | None = None
    supplier: str | None = None
    details: list[EBolDetailRequest] = Field(default_factory=list)
    request_id: str | None = None
    reference_id: str | None = None


class CallbackRouter:
    """FastAPI router for receiving BOL scanning callbacks (stateless)."""

    def __init__(
        self, callback_auth_token: str, callback_handler: Callable[[EBolRequest], None]
    ) -> None:
        """
        Initialize callback router.

        Args:
            callback_auth_token: Static Bearer token for callback validation
            callback_handler: Handler function called for all callbacks.
                            Receives EBolRequest as argument.
        """
        import asyncio

        try:
            from fastapi import APIRouter, Header, HTTPException, Request
        except ImportError:
            raise ImportError(
                "FastAPI not installed. Install with: pip install gravi-vision"
            )

        self.callback_auth_token = callback_auth_token
        self.callback_handler = callback_handler
        self.router = APIRouter(prefix="/gravi-vision-callbacks", tags=["callbacks"])

        @self.router.post("/{request_id}")
        async def receive_callback(
            request_id: str,
            request: Request,
            authorization: str = Header(None),
        ):
            """Receive callback from Gravi-Vision API."""
            if not authorization:
                raise HTTPException(status_code=403, detail="Missing authorization")

            expected = f"Bearer {callback_auth_token}"
            if not secrets.compare_digest(authorization, expected):
                raise HTTPException(status_code=403, detail="Invalid token")

            data = await request.json()
            bol_request = EBolRequest(**data)

            try:
                result = callback_handler(bol_request)
                # Support both sync and async handlers
                if asyncio.iscoroutine(result):
                    await result

                return {"status": "success"}
            except Exception as e:
                return {"status": "error", "error": str(e)}

    def get_router(self):
        """Get FastAPI router for mounting."""
        return self.router


class GraviVisionClient:
    """Client for Gravi-Vision API."""

    def __init__(
        self,
        api_key: str,
        base_url: str = "https://vision-dev.gravitate.energy",
        callback_url: Optional[str] = None,
        callback_auth_token: Optional[str] = None,
        default_callback_handler: Optional[Callable] = None,
    ) -> None:
        """
        Initialize the Gravi-Vision client.

        Args:
            api_key: API key for authentication
            base_url: Base URL of the Gravi-Vision API
            callback_url: Base URL for async callbacks (e.g., https://myservice.com).
                         If not provided, will auto-detect from app base_url when router is mounted.
            callback_auth_token: Static Bearer token for callback validation
            default_callback_handler: Default handler function for async callbacks.
                                     Can be overridden per request in scan_bol_async().

        Example:
            def handle_bol(bol_data: EBolRequest):
                save_to_database(bol_data)

            client = GraviVisionClient(
                api_key="key",
                callback_auth_token="token123",
                default_callback_handler=handle_bol,
            )

            app.include_router(client.get_callback_router())

            # Later, use default handler
            client.scan_bol_async("bol.pdf")

            # Or override with different handler
            client.scan_bol_async("bol.pdf", callback=different_handler)
        """
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.callback_url = callback_url.rstrip("/") if callback_url else None
        self.callback_auth_token = callback_auth_token
        self.default_callback_handler = default_callback_handler
        self.client = httpx.Client(
            headers={"x-api-key": api_key},
            timeout=30.0,
        )
        self._callback_router: Optional[CallbackRouter] = None

    def __enter__(self) -> "GraviVisionClient":
        """Context manager entry."""
        return self

    def __exit__(self, *args: object) -> None:
        """Context manager exit."""
        self.close()

    def close(self) -> None:
        """Close the HTTP client."""
        self.client.close()

    def get_callback_router(self):
        """
        Get FastAPI router for mounting in user's application (stateless).

        The router handles incoming callbacks from the Gravi-Vision API and
        calls the configured handler function. No per-request state is stored.

        Returns:
            FastAPI APIRouter for callback endpoint (prefix: /gravi-vision-callbacks)

        Raises:
            RuntimeError: If callback configuration missing

        Example:
            from fastapi import FastAPI
            from gravi_vision import GraviVisionClient

            app = FastAPI()

            def handle_bol_result(bol_data):
                # Use reference_id to correlate with your system
                order = db.get_order(bol_data.reference_id)
                order.bol_number = bol_data.bol_number
                db.save(order)

            client = GraviVisionClient(
                api_key="key",
                callback_auth_token="static-token",
                default_callback_handler=handle_bol_result,
            )

            # Mount router in your app
            app.include_router(client.get_callback_router())

            # Now you can use callbacks
            @app.post("/scan")
            async def scan_bol(file: UploadFile, order_id: str):
                result = client.scan_bol_async(file.file, reference_id=order_id)
                return result
        """
        if not self.callback_auth_token or not self.default_callback_handler:
            raise RuntimeError(
                "Callbacks not configured. Provide both callback_auth_token and "
                "default_callback_handler when initializing GraviVisionClient."
            )

        if self._callback_router is None:
            self._callback_router = CallbackRouter(
                self.callback_auth_token, self.default_callback_handler
            )

        return self._callback_router.get_router()

    def health_check(self) -> dict[str, str]:
        """
        Check API health status.

        Returns:
            Health status response

        Raises:
            httpx.HTTPError: If the request fails
        """
        response = self.client.get(f"{self.base_url}/health")
        response.raise_for_status()
        return response.json()

    def scan_bol(
        self,
        bol_files: str | Path | BinaryIO | tuple[BinaryIO, str] | list[str | Path | BinaryIO | tuple[BinaryIO, str]],
        reference_id: Optional[str] = None,
    ) -> EBolRequest:
        """
        Scan BOL image(s) and extract structured data (synchronous).

        Args:
            bol_files: Single file path, file-like object, tuple of (file-like, filename), or list of any of these.
                      Examples:
                        - "path/to/bol.pdf"
                        - Path("bol.pdf")
                        - file_like_object
                        - (BytesIO(data), "bol.pdf")  # Custom filename
                        - ["file1.pdf", file_like_object, (BytesIO(data), "file3.pdf")]
            reference_id: Optional reference ID for correlating with your business data

        Returns:
            Structured EBolRequest object (includes request_id and reference_id if provided)

        Raises:
            httpx.HTTPError: If the request fails
        """
        if not isinstance(bol_files, list):
            bol_files = [bol_files]

        files = []
        opened_files = []
        for idx, bol_file in enumerate(bol_files):
            if isinstance(bol_file, tuple):
                # Handle (file-like, filename) tuples
                file_obj, filename = bol_file
                if hasattr(file_obj, "seek"):
                    file_obj.seek(0)
                files.append(("files", (filename, file_obj)))
            elif isinstance(bol_file, (str, Path)):
                file_obj = open(bol_file, "rb")
                opened_files.append(file_obj)
                files.append(("files", (Path(bol_file).name, file_obj)))
            else:
                if hasattr(bol_file, "seek"):
                    bol_file.seek(0)
                filename = getattr(bol_file, "name", f"bol_{idx}.pdf")
                files.append(("files", (filename, bol_file)))

        try:
            data = {}
            if reference_id:
                data["reference_id"] = reference_id

            response = self.client.post(
                f"{self.base_url}/api/v1/scan-bol",
                files=files,
                data=data if data else None,
            )
            response.raise_for_status()
            return EBolRequest(**response.json())
        finally:
            for file_obj in opened_files:
                file_obj.close()

    def scan_bol_async(
        self,
        bol_files: str | Path | BinaryIO | tuple[BinaryIO, str] | list[str | Path | BinaryIO | tuple[BinaryIO, str]],
        reference_id: Optional[str] = None,
    ) -> dict[str, Optional[str]]:
        """
        Scan BOL image(s) asynchronously with callback.

        Returns immediately and processes in background on server.
        When complete, server POSTs result to callback endpoint, which calls the
        handler configured at client initialization (stateless, K8s-friendly).

        Args:
            bol_files: Single file path, file-like object, tuple of (file-like, filename), or list of any of these.
                      Examples:
                        - "path/to/bol.pdf"
                        - Path("bol.pdf")
                        - file_like_object
                        - (BytesIO(data), "bol.pdf")  # Custom filename for BytesIO
                        - ["file1.pdf", file_like_object, (BytesIO(data), "file3.pdf")]
            reference_id: Optional reference ID for correlating with your business data
                         (e.g., order_id, shipment_id). Will be echoed back in callback
                         for the handler to process.

        Returns:
            Dictionary with request_id and reference_id for tracking

        Raises:
            RuntimeError: If callbacks not configured or handler not provided at init
            httpx.HTTPError: If the request fails

        Example (file path):
            result = client.scan_bol_async("bol.pdf", reference_id="order-123")

        Example (BytesIO with custom filename):
            from io import BytesIO
            file_bytes = b"..."
            file_io = BytesIO(file_bytes)
            result = client.scan_bol_async(
                (file_io, "invoice_12345.pdf"),
                reference_id="order-123"
            )
        """
        if not self.callback_url or not self.callback_auth_token:
            raise RuntimeError(
                "Async scanning requires callback configuration. "
                "Provide callback_url and callback_auth_token to GraviVisionClient."
            )

        if not self.default_callback_handler:
            raise RuntimeError(
                "Callback handler not configured. "
                "Set default_callback_handler when initializing GraviVisionClient."
            )

        if not isinstance(bol_files, list):
            bol_files = [bol_files]

        request_id = secrets.token_urlsafe(16)

        files = []
        opened_files = []
        for idx, bol_file in enumerate(bol_files):
            if isinstance(bol_file, tuple):
                # Handle (file-like, filename) tuples
                file_obj, filename = bol_file
                if hasattr(file_obj, "seek"):
                    file_obj.seek(0)
                files.append(("files", (filename, file_obj)))
            elif isinstance(bol_file, (str, Path)):
                file_obj = open(bol_file, "rb")
                opened_files.append(file_obj)
                files.append(("files", (Path(bol_file).name, file_obj)))
            else:
                if hasattr(bol_file, "seek"):
                    bol_file.seek(0)
                filename = getattr(bol_file, "name", f"bol_{idx}.pdf")
                files.append(("files", (filename, bol_file)))

        try:
            callback_full_url = f"{self.callback_url}/{request_id}"
            callback_auth = f"Bearer {self.callback_auth_token}"

            response = self.client.post(
                f"{self.base_url}/api/v1/scan-bol",
                files=files,
                data={
                    "callback_url": callback_full_url,
                    "callback_auth_header": callback_auth,
                    "request_id": request_id,
                    "reference_id": reference_id,
                },
            )
            response.raise_for_status()

            return {
                "request_id": request_id,
                "reference_id": reference_id,
            }
        except Exception:
            raise
        finally:
            for file_obj in opened_files:
                file_obj.close()


__all__ = ["GraviVisionClient", "VisionResponse", "EBolRequest", "EBolDetailRequest"]
