"""
isitcredible — Python SDK
~~~~~~~~~~~~~~~~~~~~~~~~~

Usage:
    from isitcredible import Client

    client = Client("iic_your_api_key")
    report = client.analyze("paper.pdf")
    report.save("review.pdf")
"""
import os
import time
from pathlib import Path
from typing import Optional, Union

import requests


API_BASE = "https://isitcredible.com/api/v1"


class IsItCredibleError(Exception):
    """Base exception for API errors."""
    def __init__(self, message: str, status_code: int = None, detail: str = None):
        self.status_code = status_code
        self.detail = detail or message
        super().__init__(message)


class JobTimeoutError(IsItCredibleError):
    """Raised when polling exceeds the timeout."""
    pass


class Report:
    """Represents a completed analysis report."""

    def __init__(self, job: dict, client: "Client"):
        self.job_id: str = job["uuid"]
        self.status: str = job["status"]
        self.mode: str = job.get("mode", "standard")
        self.title: str = job.get("title")
        self.authors: str = job.get("authors")
        self.discipline: str = job.get("discipline")
        self.report_url: str = job.get("report_url")
        self._client = client

    def download(self, format: str = "pdf") -> bytes:
        """Download the report as bytes."""
        if not self.report_url:
            raise IsItCredibleError("No report available (job may not be completed).")
        resp = self._client._get(f"/jobs/{self.job_id}/report", params={"format": format},
                                  stream=True)
        return resp.content

    def save(self, path: Union[str, Path] = None, format: str = "pdf"):
        """Download and save the report to a file. Defaults to {job_id}.pdf in the current directory."""
        if path is None:
            path = Path(f"{self.job_id}.{format}")
        data = self.download(format=format)
        Path(path).write_bytes(data)

    def __repr__(self):
        return f"<Report job_id={self.job_id!r} status={self.status!r} title={self.title!r}>"


class Client:
    """
    isitcredible.com API client.

    Args:
        api_key: Your API key (starts with ``iic_``).
        base_url: Override the API base URL (for testing).
        timeout: HTTP request timeout in seconds.
    """

    def __init__(
        self,
        api_key: str,
        base_url: str = None,
        timeout: int = 30,
    ):
        if not api_key:
            raise IsItCredibleError("API key is required.")
        self._api_key = api_key
        self._base_url = (base_url or API_BASE).rstrip("/")
        self._timeout = timeout
        self._session = requests.Session()
        self._session.headers.update({
            "Authorization": f"Bearer {api_key}",
            "User-Agent": "isitcredible-python/0.1.1",
        })

    # -----------------------------------------------------------------
    # Core high-level method
    # -----------------------------------------------------------------

    def analyze(
        self,
        file_path: Union[str, Path],
        mode: str = "standard",
        note: str = "",
        webhook_url: str = "",
        poll_interval: int = 15,
        timeout: int = 3600,
    ) -> Report:
        """
        Submit a document for analysis and wait for the result.

        Args:
            file_path: Path to the PDF, TXT, or MD file.
            mode: ``"standard"`` or ``"extended"``.
            note: Optional note for the reviewer.
            webhook_url: Optional URL to receive a POST when the job completes.
            poll_interval: Seconds between status checks (default 15).
            timeout: Maximum seconds to wait (default 3600 = 60 min).

        Returns:
            A :class:`Report` object with the completed analysis.

        Raises:
            IsItCredibleError: On API errors.
            JobTimeoutError: If the job doesn't finish within ``timeout``.
            FileNotFoundError: If ``file_path`` doesn't exist.
        """
        job = self.submit(file_path, mode=mode, note=note, webhook_url=webhook_url)
        job_id = job["job_id"]
        return self.wait(job_id, poll_interval=poll_interval, timeout=timeout)

    # -----------------------------------------------------------------
    # Low-level methods (for advanced users / async pipelines)
    # -----------------------------------------------------------------

    def submit(
        self,
        file_path: Union[str, Path],
        mode: str = "standard",
        note: str = "",
        webhook_url: str = "",
    ) -> dict:
        """
        Submit a file for analysis without waiting.

        Returns:
            dict with ``job_id``, ``status``, ``mode``, ``message``.
        """
        file_path = Path(file_path)
        if not file_path.exists():
            raise FileNotFoundError(f"File not found: {file_path}")

        with open(file_path, "rb") as f:
            data = {"mode": mode, "user_note": note}
            if webhook_url:
                data["webhook_url"] = webhook_url
            resp = self._post(
                "/jobs",
                files={"main_file": (file_path.name, f)},
                data=data,
            )
        return resp.json()

    def wait(self, job_id: str, poll_interval: int = 15, timeout: int = 3600) -> Report:
        """
        Poll a job until completion.

        Args:
            job_id: The job UUID returned by :meth:`submit`.
            poll_interval: Seconds between polls.
            timeout: Maximum wait time in seconds.

        Returns:
            A :class:`Report` object.
        """
        deadline = time.time() + timeout

        while True:
            job = self.get_job(job_id)
            status = job.get("status", "")

            if status in ("completed", "published"):
                return Report(job, self)

            if status == "failed":
                raise IsItCredibleError(
                    f"Job {job_id} failed.",
                    detail=job.get("admin_notes", ""),
                )

            if time.time() > deadline:
                err = JobTimeoutError(
                    f"Job {job_id} did not complete within {timeout}s (status: {status})."
                )
                err.job_id = job_id
                raise err

            time.sleep(poll_interval)

    def get_job(self, job_id: str) -> dict:
        """Get status and metadata for a single job."""
        return self._get(f"/jobs/{job_id}").json()

    def list_jobs(self) -> list:
        """List your recent jobs."""
        return self._get("/jobs").json()

    def get_credits(self) -> dict:
        """Get your current credit balance."""
        return self._get("/credits").json()

    # -- Webhook management --

    def create_webhook(self, url: str, event: str = "job.completed") -> dict:
        """Register a default webhook URL. Returns the signing secret (save it!)."""
        return self._post("/webhooks", data={"url": url, "event": event}).json()

    def list_webhooks(self) -> list:
        """List your registered webhooks."""
        return self._get("/webhooks").json()

    def delete_webhook(self, webhook_id: int) -> dict:
        """Delete a webhook by ID."""
        return self._delete(f"/webhooks/{webhook_id}").json()

    # -----------------------------------------------------------------
    # HTTP helpers
    # -----------------------------------------------------------------

    def _get(self, path: str, params: dict = None, stream: bool = False) -> requests.Response:
        resp = self._session.get(
            f"{self._base_url}{path}",
            params=params,
            timeout=self._timeout,
            stream=stream,
        )
        self._check(resp)
        return resp

    def _post(self, path: str, **kwargs) -> requests.Response:
        kwargs.setdefault("timeout", self._timeout)
        resp = self._session.post(f"{self._base_url}{path}", **kwargs)
        self._check(resp)
        return resp

    def _delete(self, path: str) -> requests.Response:
        resp = self._session.delete(f"{self._base_url}{path}", timeout=self._timeout)
        self._check(resp)
        return resp

    @staticmethod
    def _check(resp: requests.Response):
        if resp.status_code >= 400:
            try:
                detail = resp.json().get("detail", resp.text)
            except Exception:
                detail = resp.text
            raise IsItCredibleError(
                f"HTTP {resp.status_code}: {detail}",
                status_code=resp.status_code,
                detail=detail,
            )
