from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Any

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry


class ApiError(RuntimeError):
    def __init__(self, message: str, status_code: int | None = None, payload: Any | None = None) -> None:
        super().__init__(message)
        self.status_code = status_code
        self.payload = payload


@dataclass(frozen=True)
class TimeoutConfig:
    connect: float = 10.0
    read: float = 30.0
    validation_read: float = 120.0


class AppraisalForgeClient:
    def __init__(
        self,
        api_url: str,
        token: str,
        timeout: float = 30.0,
        retries: int = 3,
    ) -> None:
        if not api_url:
            raise ValueError("api_url is required")
        if not token:
            raise ValueError("token is required")

        self.api_url = api_url.rstrip("/")
        self.token = token
        self.timeouts = TimeoutConfig(connect=10.0, read=timeout)

        self.session = requests.Session()
        retry = Retry(
            total=retries,
            backoff_factor=1,
            status_forcelist=[429, 502, 503, 504],
            allowed_methods=["GET", "POST", "PUT", "DELETE"],
        )
        adapter = HTTPAdapter(max_retries=retry)
        self.session.mount("http://", adapter)
        self.session.mount("https://", adapter)
        self.session.headers.update(
            {
                "Authorization": f"Bearer {self.token}",
                "Accept": "application/json",
            }
        )

    def create_appraisal(self, name: str, subject_address: str) -> dict[str, Any]:
        return self._request(
            "POST",
            "/api/appraisals/",
            json={"name": name, "subject_address": subject_address},
        )

    def read_fields(
        self,
        appraisal_id: int,
        prefix: str | None = None,
        section: str | None = None,
        search: str | None = None,
        include_empty: bool | None = None,
    ) -> dict[str, Any]:
        params: dict[str, Any] = {}
        if prefix:
            params["prefix"] = prefix
        if section:
            params["section"] = section
        if search:
            params["search"] = search
        if include_empty is not None:
            params["include_empty"] = str(include_empty).lower()
        return self._request("GET", f"/api/appraisals/{appraisal_id}/fields/", params=params)

    def write_fields(self, appraisal_id: int, fields: dict[str, Any]) -> dict[str, Any]:
        return self._request("PUT", f"/api/appraisals/{appraisal_id}/fields/", json=fields)

    def validate(self, appraisal_id: int) -> dict[str, Any]:
        return self._request(
            "GET",
            f"/api/appraisals/{appraisal_id}/validate/",
            timeout=(self.timeouts.connect, self.timeouts.validation_read),
        )

    def upload_image(
        self,
        appraisal_id: int,
        field_id: str,
        image_path: str,
        label: str | None = None,
    ) -> dict[str, Any]:
        path = Path(image_path)
        if not path.exists() or not path.is_file():
            raise FileNotFoundError(f"Image not found: {image_path}")

        data = {
            "appraisal_id": str(appraisal_id),
            "field_id": field_id,
        }
        if label:
            data["label"] = label

        with path.open("rb") as fh:
            files = {"image": (path.name, fh)}
            return self._request("POST", "/api/images/upload/", data=data, files=files)

    def list_workfiles(self, appraisal_id: int) -> dict[str, Any]:
        return self._request("GET", f"/api/appraisals/{appraisal_id}/workfiles/")

    def upload_workfile(
        self,
        appraisal_id: int,
        file_path: str,
        folder: str | None = None,
        remote_name: str | None = None,
    ) -> dict[str, Any]:
        path = Path(file_path)
        if not path.exists() or not path.is_file():
            raise FileNotFoundError(f"Workfile not found: {file_path}")

        data: dict[str, Any] = {}
        if folder:
            data["folder"] = folder

        upload_name = remote_name or path.name
        with path.open("rb") as fh:
            files = {"file": (upload_name, fh)}
            return self._request(
                "POST",
                f"/api/appraisals/{appraisal_id}/workfiles/upload/",
                data=data,
                files=files,
            )

    def get_schema(
        self,
        prefix: str | None = None,
        search: str | None = None,
        section: str | None = None,
    ) -> dict[str, Any]:
        params: dict[str, Any] = {}
        if prefix:
            params["prefix"] = prefix
        if search:
            params["search"] = search
        if section:
            params["section"] = section
        return self._request("GET", "/api/fields/schema/", params=params)

    def get_enumerations(self, field_id: str) -> dict[str, Any]:
        return self._request("GET", f"/api/fields/enumerations/{field_id}/")

    def list_appraisals(self) -> dict[str, Any]:
        return self._request("GET", "/api/appraisals/")

    def get_appraisal(self, appraisal_id: int) -> dict[str, Any]:
        return self._request("GET", f"/api/appraisals/{appraisal_id}/")

    def list_sections(self) -> dict[str, Any]:
        return self._request("GET", "/api/fields/sections/")

    def geocode(
        self,
        street: str,
        city: str,
        state: str,
        zip_code: str,
    ) -> dict[str, Any]:
        params = {"street": street, "city": city, "state": state, "zip": zip_code}
        return self._request("GET", "/api/geocode/", params=params)

    def auto_fix(self, appraisal_id: int) -> dict[str, Any]:
        return self._request(
            "POST",
            f"/api/appraisals/{appraisal_id}/validate/fix/",
            timeout=(self.timeouts.connect, self.timeouts.validation_read),
        )

    def list_images(self, appraisal_id: int) -> dict[str, Any]:
        return self._request("GET", f"/api/appraisals/{appraisal_id}/images/")

    def delete_image(self, image_id: int) -> dict[str, Any]:
        return self._request("DELETE", f"/api/images/{image_id}/")

    # Token management and connectivity helpers

    def list_tokens(self) -> dict[str, Any]:
        return self._request("GET", "/api/tokens/")

    def create_token(self, label: str = "Appraisal Forge SDK", scope: str = "sdk") -> dict[str, Any]:
        return self._request("POST", "/api/tokens/", json={"label": label, "scope": scope})

    def revoke_token(self, token_id: int) -> dict[str, Any]:
        return self._request("DELETE", f"/api/tokens/{token_id}/revoke/")

    def get_connection_info(self) -> dict[str, Any]:
        return self._request("GET", "/api/connection-info/")

    def verify_token(self) -> dict[str, Any]:
        return self._request("GET", "/api/tokens/verify/")

    def assistant_bootstrap(self, rotate: bool = False, label: str = "Appraisal Forge Auto Connect") -> dict[str, Any]:
        return self._request(
            "POST",
            "/api/assistant/bootstrap/",
            json={"rotate": bool(rotate), "label": label},
        )

    def relay_status(self) -> dict[str, Any]:
        return self._request("GET", "/api/assistant/relay/status/")

    def relay_poll_messages(
        self,
        appraisal_id: int | None = None,
        limit: int = 1,
        wait_seconds: int = 20,
    ) -> dict[str, Any]:
        params: dict[str, Any] = {
            "limit": max(1, int(limit)),
            "wait_seconds": max(0, int(wait_seconds)),
        }
        if appraisal_id is not None:
            params["appraisal_id"] = appraisal_id
        return self._request("GET", "/api/assistant/relay/poll/", params=params)

    def relay_post_reply(
        self,
        message_id: str,
        text: str,
        appraisal_id: int | None = None,
    ) -> dict[str, Any]:
        payload: dict[str, Any] = {"message_id": message_id, "text": text}
        if appraisal_id is not None:
            payload["appraisal_id"] = appraisal_id
        return self._request("POST", "/api/assistant/relay/reply/", json=payload)

    def relay_runtime_status(self) -> dict[str, Any]:
        return self._request("GET", "/api/assistant/relay/runtime/status/")

    def relay_runtime_start(
        self,
        agent_cmd: str,
        appraisal_id: int | None = None,
        base_url: str | None = None,
    ) -> dict[str, Any]:
        payload: dict[str, Any] = {"agent_cmd": agent_cmd}
        if appraisal_id is not None:
            payload["appraisal_id"] = appraisal_id
        if base_url:
            payload["base_url"] = base_url
        return self._request("POST", "/api/assistant/relay/runtime/start/", json=payload)

    def relay_runtime_stop(self) -> dict[str, Any]:
        return self._request("POST", "/api/assistant/relay/runtime/stop/", json={})

    @staticmethod
    def start_device_login(
        api_url: str,
        scope: str = "full",
        label: str = "CLI Device Login",
        client_name: str = "appraisal-forge-sdk",
        timeout: float = 30.0,
    ) -> dict[str, Any]:
        session = requests.Session()
        url = api_url.rstrip("/") + "/api/auth/device/start/"
        response = session.post(
            url,
            json={"scope": scope, "label": label, "client_name": client_name},
            headers={"Content-Type": "application/json", "Accept": "application/json"},
            timeout=(10.0, timeout),
        )
        if response.status_code >= 400:
            try:
                payload = response.json()
            except ValueError:
                payload = response.text
            raise ApiError("POST /api/auth/device/start/ failed", status_code=response.status_code, payload=payload)
        return response.json()

    @staticmethod
    def poll_device_token(
        api_url: str,
        device_code: str,
        timeout: float = 30.0,
    ) -> dict[str, Any]:
        session = requests.Session()
        url = api_url.rstrip("/") + "/api/auth/device/token/"
        response = session.post(
            url,
            json={"device_code": device_code},
            headers={"Content-Type": "application/json", "Accept": "application/json"},
            timeout=(10.0, timeout),
        )
        if response.status_code >= 400:
            try:
                payload = response.json()
            except ValueError:
                payload = response.text
            raise ApiError("POST /api/auth/device/token/ failed", status_code=response.status_code, payload=payload)
        return response.json()

    def create_adjustment_session(
        self,
        appraisal_id: int,
        name: str = "",
        state: str = "",
        property_type: str = "SFR",
        effective_date: str = "",
        subject_data: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        body: dict[str, Any] = {"appraisal_id": appraisal_id}
        if name:
            body["name"] = name
        if state:
            body["state"] = state
        if property_type:
            body["property_type"] = property_type
        if effective_date:
            body["effective_date"] = effective_date
        if subject_data:
            body["subject_data"] = subject_data
        return self._request("POST", "/api/adjustments/sessions/", json=body)

    def upload_comp_data(self, session_id: int, csv_path: str) -> dict[str, Any]:
        path = Path(csv_path)
        if not path.exists() or not path.is_file():
            raise FileNotFoundError(f"CSV file not found: {csv_path}")

        with path.open("rb") as fh:
            files = {"file": (path.name, fh)}
            return self._request(
                "POST",
                f"/api/adjustments/sessions/{session_id}/datasets/",
                files=files,
            )

    def run_calculations(self, session_id: int) -> dict[str, Any]:
        return self._request("POST", f"/api/adjustments/sessions/{session_id}/calculate/")

    def apply_adjustments(self, session_id: int) -> dict[str, Any]:
        return self._request("POST", f"/api/adjustments/sessions/{session_id}/apply/")

    def upload_comp_data_text(
        self,
        session_id: int,
        csv_text: str,
        dataset_type: str = "comps",
    ) -> dict[str, Any]:
        """Upload comp data as raw CSV text (not a file)."""
        return self._request(
            "POST",
            f"/api/adjustments/sessions/{session_id}/datasets/",
            json={"csv_text": csv_text, "dataset_type": dataset_type},
        )

    def set_feature_value(
        self,
        session_id: int,
        feature_id: int,
        chosen_value: float,
        is_done: bool = True,
    ) -> dict[str, Any]:
        """Set the chosen adjustment value for a feature."""
        return self._request(
            "PUT",
            f"/api/adjustments/sessions/{session_id}/features/{feature_id}/",
            json={"chosen_value": chosen_value, "is_done": is_done},
        )

    def get_apply_data(self, session_id: int) -> dict[str, Any]:
        """Get the fields_to_set dict from an adjustment session without writing."""
        return self._request("POST", f"/api/adjustments/sessions/{session_id}/apply/")

    def _request(self, method: str, path: str, **kwargs: Any) -> dict[str, Any]:
        url = f"{self.api_url}{path}"

        if "files" in kwargs:
            headers = kwargs.pop("headers", {})
            kwargs["headers"] = headers
        else:
            headers = kwargs.pop("headers", {})
            headers.setdefault("Content-Type", "application/json")
            kwargs["headers"] = headers

        kwargs.setdefault("timeout", (self.timeouts.connect, self.timeouts.read))

        response = self.session.request(method, url, **kwargs)
        if response.status_code >= 400:
            payload: Any
            try:
                payload = response.json()
            except ValueError:
                payload = response.text
            raise ApiError(
                f"{method} {path} failed with {response.status_code}",
                status_code=response.status_code,
                payload=payload,
            )

        if not response.content:
            return {}

        try:
            data = response.json()
        except ValueError as exc:
            raise ApiError(f"{method} {path} returned non-JSON response", payload=response.text) from exc
        if not isinstance(data, dict):
            return {"data": data}
        return data
