import time
from typing import Any, Dict, Optional

import requests

from zyndpay.errors import (
    AuthenticationError,
    ConflictError,
    NotFoundError,
    RateLimitError,
    ValidationError,
    ZyndPayError,
)

DEFAULT_BASE_URL = "https://api.zyndpay.io/v1"
DEFAULT_TIMEOUT = 30
DEFAULT_MAX_RETRIES = 2
RETRYABLE_STATUS_CODES = {408, 429, 500, 502, 503, 504}


class HttpClient:
    def __init__(
        self,
        api_key: str,
        base_url: str = None,
        timeout: int = None,
        max_retries: int = None,
    ):
        if not api_key:
            raise AuthenticationError("API key is required")

        self.api_key = api_key
        self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
        self.timeout = timeout or DEFAULT_TIMEOUT
        self.max_retries = max_retries if max_retries is not None else DEFAULT_MAX_RETRIES
        self.session = requests.Session()
        self.session.headers.update(
            {
                "X-Api-Key": self.api_key,
                "Content-Type": "application/json",
                "User-Agent": "zyndpay-python/1.4.0",
                "X-ZyndPay-Api-Version": "2024-01-01",
            }
        )

    def request(
        self,
        method: str,
        path: str,
        json: Any = None,
        params: Dict[str, Any] = None,
        headers: Dict[str, str] = None,
        idempotency_key: str = None,
    ) -> Dict[str, Any]:
        url = f"{self.base_url}{path}"

        # Filter out None params
        if params:
            params = {k: v for k, v in params.items() if v is not None}

        extra_headers = dict(headers or {})
        if idempotency_key:
            extra_headers["Idempotency-Key"] = idempotency_key

        last_error = None
        last_response = None

        for attempt in range(self.max_retries + 1):
            try:
                response = self.session.request(
                    method,
                    url,
                    json=json,
                    params=params,
                    headers=extra_headers,
                    timeout=self.timeout,
                )

                if response.ok:
                    return response.json()

                request_id = response.headers.get("x-request-id")

                if response.status_code not in RETRYABLE_STATUS_CODES:
                    body = response.json() if response.text else {}
                    raise self._create_error(response.status_code, body, request_id)

                if response.status_code == 429:
                    retry_after = int(response.headers.get("Retry-After", "1"))
                    if attempt < self.max_retries:
                        time.sleep(retry_after)
                        continue
                    body = response.json() if response.text else {}
                    raise self._create_error(response.status_code, body, request_id)

                last_response = response
                last_error = ZyndPayError(
                    f"Request failed with status {response.status_code}",
                    response.status_code,
                    request_id,
                )

                if attempt < self.max_retries:
                    time.sleep(2**attempt * 0.5)

            except ZyndPayError:
                raise
            except Exception as e:
                last_error = e
                if attempt < self.max_retries:
                    time.sleep(2**attempt * 0.5)

        if last_response is not None:
            try:
                body = last_response.json() if last_response.text else {}
            except (ValueError, Exception):
                body = {"message": last_response.text or f"Request failed with status {last_response.status_code}"}
            request_id = last_response.headers.get("x-request-id")
            raise self._create_error(last_response.status_code, body, request_id)

        raise last_error or ZyndPayError("Request failed after retries")

    def get(self, path: str, params: Dict[str, Any] = None) -> Dict[str, Any]:
        return self.request("GET", path, params=params)

    def post(
        self,
        path: str,
        json: Any = None,
        idempotency_key: str = None,
        params: Dict[str, Any] = None,
    ) -> Dict[str, Any]:
        return self.request("POST", path, json=json, idempotency_key=idempotency_key, params=params)

    def post_file(
        self,
        path: str,
        file_path: str,
        field_name: str = "file",
    ) -> Dict[str, Any]:
        """Upload a file via multipart form data."""
        url = f"{self.base_url}{path}"
        with open(file_path, "rb") as f:
            # Don't send Content-Type header — requests sets it for multipart
            headers = {
                "X-Api-Key": self.api_key,
                "User-Agent": "zyndpay-python/1.4.0",
                "X-ZyndPay-Api-Version": "2024-01-01",
            }
            response = requests.post(
                url,
                files={field_name: f},
                headers=headers,
                timeout=self.timeout,
            )
        if response.ok:
            return response.json()
        request_id = response.headers.get("x-request-id")
        try:
            body = response.json() if response.text else {}
        except (ValueError, Exception):
            body = {"message": response.text or f"Request failed with status {response.status_code}"}
        raise self._create_error(response.status_code, body, request_id)

    def get_raw(self, path: str, params: Dict[str, Any] = None) -> str:
        """Fetch a raw text response (e.g. CSV export, template download)."""
        url = f"{self.base_url}{path}"
        if params:
            params = {k: v for k, v in params.items() if v is not None}
        headers = {
            "X-Api-Key": self.api_key,
            "User-Agent": "zyndpay-python/1.4.0",
            "X-ZyndPay-Api-Version": "2024-01-01",
        }
        response = self.session.get(url, params=params, headers=headers, timeout=self.timeout)
        if response.ok:
            return response.text
        request_id = response.headers.get("x-request-id")
        try:
            body = response.json() if response.text else {}
        except (ValueError, Exception):
            body = {"message": response.text or f"Request failed with status {response.status_code}"}
        raise self._create_error(response.status_code, body, request_id)

    def patch(self, path: str, json: Any = None) -> Dict[str, Any]:
        return self.request("PATCH", path, json=json)

    def delete(self, path: str) -> Dict[str, Any]:
        return self.request("DELETE", path)

    @staticmethod
    def _create_error(
        status_code: int, body: dict, request_id: Optional[str]
    ) -> ZyndPayError:
        error_obj = body.get("error") if isinstance(body, dict) else None
        if isinstance(error_obj, dict):
            message = error_obj.get("message", f"Request failed with status {status_code}")
            code = error_obj.get("code")
            details = error_obj.get("details")
        else:
            message = (body.get("message") if isinstance(body, dict) else None) or \
                      (error_obj if isinstance(error_obj, str) else None) or \
                      f"Request failed with status {status_code}"
            code = None
            details = None

        if status_code == 400 or status_code == 422:
            return ValidationError(message, request_id=request_id, code=code or 'VALIDATION_ERROR', details=details, raw_body=body)
        elif status_code == 401:
            return AuthenticationError(message, request_id=request_id, code=code or 'UNAUTHORIZED', raw_body=body)
        elif status_code == 403:
            return AuthenticationError(message, request_id=request_id, code=code or 'FORBIDDEN', raw_body=body)
        elif status_code == 404:
            return NotFoundError(message, request_id=request_id, code=code or 'NOT_FOUND', raw_body=body)
        elif status_code == 409:
            return ConflictError(message, request_id=request_id, code=code or 'CONFLICT', raw_body=body)
        elif status_code == 429:
            return RateLimitError(message, request_id=request_id, code=code or 'RATE_LIMIT_EXCEEDED', raw_body=body)
        else:
            return ZyndPayError(message, status_code=status_code, request_id=request_id, code=code, raw_body=body)
