"""
Synchronous HTTP client for the seq_interp service (port 7475 by default).

All methods are blocking and intended to be called from a QThread worker.
"""
from __future__ import annotations

import os

from src.clients import extract_error_text


class SeqInterpError(Exception):
    def __init__(self, message: str, status_code: int | None = None) -> None:
        super().__init__(message)
        self.status_code = status_code


class SeqInterpClient:
    """Thin wrapper around the seq_interp REST API."""

    def __init__(self, base_url: str = "http://localhost:7475") -> None:
        self.base_url = base_url.rstrip("/")

    # -- internals ---------------------------------------------------------

    def _get(self, path: str) -> dict:
        import httpx
        try:
            r = httpx.get(
                f"{self.base_url}{path}",
                timeout=httpx.Timeout(connect=3.0, read=120.0, write=10.0, pool=5.0),
            )
        except httpx.ConnectError as exc:
            raise SeqInterpError(
                f"Cannot connect to seq_interp at {self.base_url}"
            ) from exc
        except httpx.TimeoutException as exc:
            raise SeqInterpError("seq_interp request timed out") from exc
        if not r.is_success:
            raise SeqInterpError(extract_error_text(r), status_code=r.status_code)
        return r.json()

    def _post_file(self, path: str, file_path: str,
                   data: dict | None = None) -> dict:
        import httpx
        filename = os.path.basename(file_path)
        try:
            with open(file_path, "rb") as fh:
                r = httpx.post(
                    f"{self.base_url}{path}",
                    files={"file": (filename, fh, "application/octet-stream")},
                    data=data or {},
                    timeout=httpx.Timeout(connect=3.0, read=120.0, write=30.0, pool=5.0),
                )
        except httpx.ConnectError as exc:
            raise SeqInterpError(
                f"Cannot connect to seq_interp at {self.base_url}"
            ) from exc
        except httpx.TimeoutException as exc:
            raise SeqInterpError("seq_interp upload timed out") from exc
        if not r.is_success:
            raise SeqInterpError(extract_error_text(r), status_code=r.status_code)
        return r.json()

    # -- public API --------------------------------------------------------

    def healthcheck(self) -> bool:
        """Return True if the service responds on /health."""
        try:
            self._get("/health")
            return True
        except SeqInterpError:
            return False

    def interpret(self, seq_file_path: str,
                  hw_overrides: dict | None = None) -> str:
        """
        Upload a .seq file and start interpretation. Returns task_id.

        hw_overrides : optional HardwareConstraints overrides (delay values and
                       TR/RF/START enable flags) applied server-side.
        """
        import json
        form = {"hw_overrides": json.dumps(hw_overrides)} if hw_overrides else None
        data = self._post_file("/interpret/", seq_file_path, data=form)
        return data["task_id"]

    def get_status(self) -> dict:
        """Return status dict for all submitted tasks."""
        return self._get("/status/")

    def export_artifacts(self, task_id: str, dest_dir: str) -> str:
        """
        Download the task's artifacts zip and extract it into dest_dir.
        Returns dest_dir. Used by the GUI Export action.
        """
        import io
        import zipfile

        import httpx
        try:
            r = httpx.get(
                f"{self.base_url}/artifacts/{task_id}",
                timeout=httpx.Timeout(connect=3.0, read=120.0, write=10.0, pool=5.0),
            )
        except httpx.ConnectError as exc:
            raise SeqInterpError(
                f"Cannot connect to seq_interp at {self.base_url}"
            ) from exc
        except httpx.TimeoutException as exc:
            raise SeqInterpError("seq_interp artifact download timed out") from exc
        if not r.is_success:
            raise SeqInterpError(extract_error_text(r), status_code=r.status_code)
        os.makedirs(dest_dir, exist_ok=True)
        with zipfile.ZipFile(io.BytesIO(r.content)) as zf:
            zf.extractall(dest_dir)
        return dest_dir

    def get_result(self, task_id: str) -> dict:
        """
        Return the full interpretation result for task_id.
        Raises SeqInterpError(status_code=202) if still processing.
        Raises SeqInterpError(status_code=404) if task not found.
        """
        return self._get(f"/result/{task_id}")

    def interpret_and_wait(
        self,
        seq_file_path: str,
        poll_interval: float = 1.0,
        max_attempts: int = 120,
        progress_cb=None,
        hw_overrides: dict | None = None,
    ) -> dict:
        """
        Upload .seq, poll until done, return result dict.

        progress_cb: optional callable(str) for log messages.
        hw_overrides: optional HardwareConstraints overrides applied server-side.
        """
        import time

        def _log(msg: str) -> None:
            if progress_cb:
                progress_cb(msg)

        _log(f"Uploading {os.path.basename(seq_file_path)}...")
        task_id = self.interpret(seq_file_path, hw_overrides=hw_overrides)
        _log(f"Task accepted: {task_id}")

        for attempt in range(max_attempts):
            time.sleep(poll_interval)
            status = self.get_status().get("tasks", {}).get(task_id, "unknown")
            _log(f"Status: {status}")
            if status == "completed":
                _log("Fetching result...")
                return self.get_result(task_id)
            if status.startswith("failed"):
                raise SeqInterpError(f"Interpretation failed: {status}")

        raise SeqInterpError(
            f"Interpretation timed out after {max_attempts * poll_interval:.0f}s"
        )
