from __future__ import annotations

import httpx
from typing import Literal


class APIStatusError(Exception):
    """Raised when an API response has a status code of 4xx or 5xx."""

    response: httpx.Response
    status_code: int
    request_id: str | None

    def __init__(
        self, message: str, *, response: httpx.Response, body: object | None
    ) -> None:
        super().__init__(message)
        self.request = response.request
        self.body = body
        self.response = response
        self.status_code = response.status_code
        self.request_id = response.headers.get("x-request-id")


class APIConnectionError(Exception):
    def __init__(
        self, *, message: str = "Connection error.", request: httpx.Request | None
    ) -> None:
        super().__init__(message)
        self.request = request


class BadRequestError(APIStatusError):
    status_code: Literal[400] = 400  # pyright: ignore[reportIncompatibleVariableOverride]


class AuthenticationError(APIStatusError):
    status_code: Literal[401] = 401  # pyright: ignore[reportIncompatibleVariableOverride]


class PermissionDeniedError(APIStatusError):
    status_code: Literal[403] = 403  # pyright: ignore[reportIncompatibleVariableOverride]


class NotFoundError(APIStatusError):
    status_code: Literal[404] = 404  # pyright: ignore[reportIncompatibleVariableOverride]


class ConflictError(APIStatusError):
    status_code: Literal[409] = 409  # pyright: ignore[reportIncompatibleVariableOverride]


class UnprocessableEntityError(APIStatusError):
    status_code: Literal[422] = 422  # pyright: ignore[reportIncompatibleVariableOverride]


class RateLimitError(APIStatusError):
    status_code: Literal[429] = 429  # pyright: ignore[reportIncompatibleVariableOverride]


class APITimeoutError(APIConnectionError):
    def __init__(self, request: httpx.Request | None) -> None:
        super().__init__(message="Request timed out.", request=request)


class StorageNotInitializedError(RuntimeError):
    """Raised when storage operations are attempted before initialization."""

    def __init__(self, storage_type: str = "Storage"):
        super().__init__(
            f"{storage_type} not initialized. Please ensure proper initialization:\n"
            f"\n"
            f"  rag = LightRAG(...)\n"
            f"  await rag.initialize_storages()  # Required - auto-initializes pipeline_status\n"
            f"\n"
            f"See: https://github.com/HKUDS/LightRAG#important-initialization-requirements"
        )


class PipelineNotInitializedError(KeyError):
    """Raised when pipeline status is accessed before initialization."""

    def __init__(self, namespace: str = ""):
        msg = (
            f"Pipeline namespace '{namespace}' not found.\n"
            f"\n"
            f"Pipeline status should be auto-initialized by initialize_storages().\n"
            f"If you see this error, please ensure:\n"
            f"\n"
            f"  1. You called await rag.initialize_storages()\n"
            f"  2. For multi-workspace setups, each LightRAG instance was properly initialized\n"
            f"\n"
            f"Standard initialization:\n"
            f"  rag = LightRAG(workspace='your_workspace')\n"
            f"  await rag.initialize_storages()  # Auto-initializes pipeline_status\n"
            f"\n"
            f"If you need manual control (advanced):\n"
            f"  from lightrag.kg.shared_storage import initialize_pipeline_status\n"
            f"  await initialize_pipeline_status(workspace='your_workspace')"
        )
        super().__init__(msg)


class PipelineCancelledException(Exception):
    """Raised when pipeline processing is cancelled by user request."""

    def __init__(self, message: str = "User cancelled"):
        super().__init__(message)
        self.message = message


class IndexFlushError(Exception):
    """Raised when a storage backend fails to flush buffered index ops.

    Carries the storage driver name and namespace so the pipeline can abort
    the batch with an actionable reason. The underlying error is preserved as
    the exception ``__cause__`` (set via ``raise ... from cause``).
    """

    def __init__(self, storage_name: str, namespace: str, cause: BaseException):
        self.storage_name = storage_name
        self.namespace = namespace
        super().__init__(f"{storage_name}[{namespace}] index flush failed: {cause}")


class ChunkTokenLimitExceededError(ValueError):
    """Raised when a chunk exceeds the configured token limit."""

    def __init__(
        self,
        chunk_tokens: int,
        chunk_token_limit: int,
        chunk_preview: str | None = None,
    ) -> None:
        preview = chunk_preview.strip() if chunk_preview else None
        truncated_preview = preview[:80] if preview else None
        preview_note = f" Preview: '{truncated_preview}'" if truncated_preview else ""
        message = (
            f"Chunk token length {chunk_tokens} exceeds chunk_token_size {chunk_token_limit}."
            f"{preview_note}"
        )
        super().__init__(message)
        self.chunk_tokens = chunk_tokens
        self.chunk_token_limit = chunk_token_limit
        self.chunk_preview = truncated_preview


class ChunkBlockMatchError(ValueError):
    """Raised when a chunk cannot be located in the document's blocks.jsonl.

    Sidecar backfill (``lightrag.sidecar.backfill``) maps F/R/V chunks back to
    their source block(s) by matching chunk content against the parse-time
    ``*.blocks.jsonl`` merged text. When a sidecar-less chunk cannot be located,
    this is raised so the pipeline marks the document FAILED rather than
    persisting chunks with missing/incorrect provenance.
    """

    def __init__(
        self,
        chunk_order_index: int,
        chunk_preview: str | None = None,
        blocks_path: str | None = None,
    ) -> None:
        preview = chunk_preview.strip() if chunk_preview else None
        truncated_preview = preview[:80] if preview else None
        preview_note = f" Preview: '{truncated_preview}'" if truncated_preview else ""
        path_note = f" (blocks: {blocks_path})" if blocks_path else ""
        message = (
            f"Chunk #{chunk_order_index} could not be located in the document "
            f"blocks during sidecar backfill.{preview_note}{path_note}"
        )
        super().__init__(message)
        self.chunk_order_index = chunk_order_index
        self.chunk_preview = truncated_preview
        self.blocks_path = blocks_path


class DataMigrationError(Exception):
    """Raised when data migration from legacy collection/table fails."""

    def __init__(self, message: str):
        super().__init__(message)
        self.message = message


class MultimodalAnalysisError(RuntimeError):
    """Raised when multimodal analysis must fail the current document.

    Hard failures (missing required field, schema mismatch, model not
    available, sidecar already carries ``status="failure"``) bubble this
    exception so the pipeline marks the document failed instead of writing
    an unusable analyze result. Callers persist a ``status="failure"``
    sidecar entry alongside the raise so a re-run sees the failure.
    """
