from __future__ import annotations

import json
import mimetypes
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Dict, Optional

import requests

from autotouch_cli.exceptions import AutotouchAPIError, AutotouchInputError

__all__ = [
    "ApiRequestError",
    "request_api",
    "request_multipart_api",
]


@dataclass(frozen=True)
class ApiRequestError(RuntimeError):
    message: str
    kind: str = "http_error"
    status_code: Optional[int] = None
    body: Any = None

    def as_dict(self) -> Dict[str, Any]:
        payload: Dict[str, Any] = {
            "kind": self.kind,
            "message": self.message,
        }
        if self.status_code is not None:
            payload["statusCode"] = int(self.status_code)
        if self.body is not None:
            payload["body"] = self.body
        return payload


def _decode_response_body(response: Any) -> Any:
    content_type = response.headers.get("content-type", "").lower()
    if "application/json" in content_type:
        try:
            return response.json()
        except Exception:
            return response.text
    return response.text


def _print_error_body(body: Any) -> None:
    if isinstance(body, (dict, list)):
        print(json.dumps(body, indent=2, default=str), file=sys.stderr)
    else:
        print(str(body), file=sys.stderr)


def request_api(
    method: str,
    path: str,
    *,
    base_url: str,
    token: Optional[str],
    api_url_fn: Callable[[Optional[str]], str],
    auth_headers_fn: Callable[..., Dict[str, str]],
    requests_module: Any = requests,
    use_x_api_key: bool = False,
    params: Optional[Dict[str, Any]] = None,
    payload: Optional[Any] = None,
    form_payload: Optional[Dict[str, Any]] = None,
    files: Optional[Any] = None,
    extra_headers: Optional[Dict[str, str]] = None,
    timeout: int = 30,
    default_timeout_seconds: int = 30,
    verbose: bool = False,
    error_body_writer: Optional[Callable[[Any], None]] = None,
    raise_on_error: bool = False,
) -> Any:
    url = f"{api_url_fn(base_url)}{path if path.startswith('/') else f'/{path}'}"
    headers = auth_headers_fn(token, use_x_api_key=use_x_api_key)
    if extra_headers:
        headers = {
            **headers,
            **{str(k): str(v) for k, v in extra_headers.items() if k and v is not None},
        }
    if verbose:
        print(f"[HTTP] {method.upper()} {url}", file=sys.stderr)
        if params:
            print(f"[HTTP] params={json.dumps(params, default=str)}", file=sys.stderr)
        if payload is not None:
            print(f"[HTTP] payload={json.dumps(payload, default=str)}", file=sys.stderr)
        if form_payload is not None:
            print(f"[HTTP] form={json.dumps(form_payload, default=str)}", file=sys.stderr)

    try:
        response = requests_module.request(
            method=method.upper(),
            url=url,
            headers=headers,
            params=params,
            json=payload,
            data=form_payload,
            files=files,
            timeout=max(1, int(timeout or default_timeout_seconds)),
        )
    except requests.RequestException as exc:
        if raise_on_error:
            raise ApiRequestError(
                message=f"request failed: {exc}",
                kind="request_exception",
            ) from exc
        print(f"ERROR: request failed: {exc}", file=sys.stderr)
        raise AutotouchAPIError(f"request failed: {exc}") from exc

    body = _decode_response_body(response)

    if response.status_code >= 300:
        if raise_on_error:
            raise ApiRequestError(
                message=f"API {response.status_code}",
                kind="http_error",
                status_code=response.status_code,
                body=body,
            )
        print(f"ERROR: API {response.status_code}", file=sys.stderr)
        if error_body_writer is not None:
            error_body_writer(body)
        else:
            _print_error_body(body)
        raise AutotouchAPIError(
            f"API {response.status_code}",
            status_code=response.status_code,
            response_body=str(body),
        )
    return body


def request_multipart_api(
    method: str,
    path: str,
    *,
    base_url: str,
    token: Optional[str],
    file_path: str,
    api_url_fn: Callable[[Optional[str]], str],
    auth_headers_fn: Callable[..., Dict[str, str]],
    requests_module: Any = requests,
    file_field: str = "file",
    content_type: Optional[str] = None,
    form_fields: Optional[Dict[str, Any]] = None,
    params: Optional[Dict[str, Any]] = None,
    use_x_api_key: bool = False,
    timeout: int = 30,
    default_timeout_seconds: int = 30,
    verbose: bool = False,
    error_body_writer: Optional[Callable[[Any], None]] = None,
    raise_on_error: bool = False,
) -> Any:
    url = f"{api_url_fn(base_url)}{path if path.startswith('/') else f'/{path}'}"
    headers = auth_headers_fn(token, use_x_api_key=use_x_api_key)
    form_payload = {
        str(k): (str(v).lower() if isinstance(v, bool) else str(v))
        for k, v in (form_fields or {}).items()
        if k is not None and v is not None
    }

    if verbose:
        print(f"[HTTP] {method.upper()} {url}", file=sys.stderr)
        if params:
            print(f"[HTTP] params={json.dumps(params, default=str)}", file=sys.stderr)
        if form_payload:
            print(f"[HTTP] form={json.dumps(form_payload, default=str)}", file=sys.stderr)

    try:
        with open(file_path, "rb") as handle:
            mime = content_type or mimetypes.guess_type(file_path)[0] or "application/octet-stream"
            files = {file_field: (Path(file_path).name, handle, mime)}
            response = requests_module.request(
                method=method.upper(),
                url=url,
                headers=headers,
                params=params,
                data=form_payload,
                files=files,
                timeout=max(1, int(timeout or default_timeout_seconds)),
            )
    except FileNotFoundError as exc:
        raise AutotouchInputError(f"file not found: {file_path}") from exc
    except requests.RequestException as exc:
        if raise_on_error:
            raise ApiRequestError(
                message=f"request failed: {exc}",
                kind="request_exception",
            ) from exc
        print(f"ERROR: request failed: {exc}", file=sys.stderr)
        raise AutotouchAPIError(f"request failed: {exc}") from exc

    body = _decode_response_body(response)

    if response.status_code >= 300:
        if raise_on_error:
            raise ApiRequestError(
                message=f"API {response.status_code}",
                kind="http_error",
                status_code=response.status_code,
                body=body,
            )
        print(f"ERROR: API {response.status_code}", file=sys.stderr)
        if error_body_writer is not None:
            error_body_writer(body)
        else:
            _print_error_body(body)
        raise AutotouchAPIError(
            f"API {response.status_code}",
            status_code=response.status_code,
            response_body=str(body),
        )
    return body
