"""
Main client implementation for the Paygent SDK.

Design Principles (Enterprise Grade):
  - ALL outbound API calls are fire-and-forget via a ThreadPoolExecutor.
    The calling thread is NEVER blocked waiting for Paygent servers.
  - Every code path is wrapped in try/except so a Paygent failure NEVER
    propagates to the user's application.
  - create_or_get_customer has a hard 1-second timeout.  If the server is
    slow or down, we absorb the error silently.
  - Strict required-field validation: agent_id, customer_id, and indicator
    must all be non-empty; if any are missing we drop the call and warn.
  - customer_name is optional; when absent we use customer_id as the name.
"""

import logging
import time
from typing import Optional
from urllib.parse import urljoin
from concurrent.futures import ThreadPoolExecutor

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

try:
    import tiktoken
except ImportError:
    tiktoken = None

from .models import (
    UsageData,
    UsageDataWithStrings,
    APIRequest,
    RawUsageData,
    VideoUsageData,
    SendUsageV2Response,
    CostBreakdown,
    Customer,
    CustomerCreateOrGetRequest,
)

_CUSTOMER_TIMEOUT_SECONDS = 1  # Hard timeout for create_or_get_customer


class Client:
    """Paygent SDK client for tracking usage and costs for AI models."""

    def __init__(self, api_key: str):
        """
        Initialize the Paygent SDK client.

        Args:
            api_key: Your Paygent API key
        """
        try:
            self.api_key = api_key
            # Locked configuration - cannot be changed by users
            # self.base_url="http://localhost:8082"
            self.base_url = "https://cp-api.withpaygent.com"

            # ----------------------------------------------------------------
            # Logging
            # ----------------------------------------------------------------
            self.logger = logging.getLogger("paygent_sdk.client")

            # ----------------------------------------------------------------
            # HTTP session — short connect/read timeouts, NO retries for the
            # tracking calls (fire-and-forget; we don't want back-pressure).
            # ----------------------------------------------------------------
            self.session = requests.Session()
            # Minimal retry: only on hard network errors, not 5xx, so we
            # never pile up retries in a background thread pool.
            retry_strategy = Retry(
                total=1,
                backoff_factor=0,
                status_forcelist=[],  # Don't retry on server errors
                raise_on_status=False,
            )
            adapter = HTTPAdapter(max_retries=retry_strategy)
            self.session.mount("http://", adapter)
            self.session.mount("https://", adapter)

            # ----------------------------------------------------------------
            # Thread pool — 10 daemon workers so they die with the process.
            # All outbound Paygent calls are submitted here.
            # ----------------------------------------------------------------
            self._executor = ThreadPoolExecutor(
                max_workers=10,
                thread_name_prefix="paygent_worker",
            )

        except Exception as e:
            # Init should never crash the host app
            try:
                self.logger.error(f"[Paygent] Client init encountered an error: {e}")
            except Exception:
                pass  # Even logging failed — absorb silently

    # ------------------------------------------------------------------
    # Public helpers
    # ------------------------------------------------------------------

    @classmethod
    def new_client(cls, api_key: str) -> "Client":
        """Create a new Paygent SDK client."""
        return cls(api_key)

    def set_log_level(self, level: int) -> None:
        """Set the logging level (e.g. logging.DEBUG, logging.INFO)."""
        try:
            self.logger.setLevel(level)
        except Exception:
            pass

    def get_logger(self) -> logging.Logger:
        """Return the SDK logger for custom configuration."""
        return self.logger

    # ------------------------------------------------------------------
    # Usage tracking — all fire-and-forget
    # ------------------------------------------------------------------

    def send_usage(
        self,
        agent_id: str,
        customer_id: str,
        indicator: str,
        usage_data: UsageData,
    ) -> None:
        """
        Send LLM usage data to Paygent (non-blocking / fire-and-forget).

        Required: agent_id, customer_id, indicator must all be non-empty.
        If any are missing the call is silently dropped and a warning is logged.
        This method NEVER raises an exception.
        """
        try:
            if not agent_id or not customer_id or not indicator:
                self.logger.warning(
                    "[Paygent] send_usage skipped: agent_id, customer_id, "
                    "and indicator are all required."
                )
                return

            raw_usage = RawUsageData(
                provider=usage_data.service_provider,
                model=usage_data.model,
                input_tokens=usage_data.prompt_tokens,
                output_tokens=usage_data.completion_tokens,
                cached_tokens=usage_data.cached_tokens,
                plan=getattr(usage_data, "plan", None),
            )
            self.send_usage_v2(agent_id, customer_id, indicator, raw_usage)

        except Exception as e:
            try:
                self.logger.error(f"[Paygent] send_usage error: {e}")
            except Exception:
                pass

    def send_usage_with_token_string(
        self,
        agent_id: str,
        customer_id: str,
        indicator: str,
        usage_data: UsageDataWithStrings,
    ) -> None:
        """
        Send usage data to Paygent using raw prompt/output strings.

        Tokens are counted locally by tiktoken (with a word-count fallback).
        Non-blocking / fire-and-forget. Never raises an exception.
        """
        try:
            if not agent_id or not customer_id or not indicator:
                self.logger.warning(
                    "[Paygent] send_usage_with_token_string skipped: "
                    "agent_id, customer_id, and indicator are all required."
                )
                return

            prompt_tokens = self._get_token_count(
                usage_data.model, usage_data.prompt_string
            )
            completion_tokens = self._get_token_count(
                usage_data.model, usage_data.output_string
            )

            raw_usage = RawUsageData(
                provider=usage_data.service_provider,
                model=usage_data.model,
                input_tokens=prompt_tokens,
                output_tokens=completion_tokens,
                plan=getattr(usage_data, "plan", None),
            )
            self.send_usage_v2(agent_id, customer_id, indicator, raw_usage)

        except Exception as e:
            try:
                self.logger.error(f"[Paygent] send_usage_with_token_string error: {e}")
            except Exception:
                pass

    def send_usage_v2(
        self,
        agent_id: str,
        customer_id: str,
        indicator: str,
        raw_usage: RawUsageData,
    ) -> SendUsageV2Response:
        """
        Core method that submits the usage payload to Paygent API v2.

        Returns immediately with a placeholder response — actual HTTP call
        runs in a background daemon thread.  Never raises an exception.
        """
        try:
            if not agent_id or not customer_id or not indicator:
                self.logger.warning(
                    "[Paygent] send_usage_v2 skipped: agent_id, customer_id, "
                    "and indicator are all required."
                )
                return SendUsageV2Response(
                    cp_data_id="",
                    calculated_cost=0.0,
                    breakdown=CostBreakdown(0.0, 0.0, 0.0, 0.0, 0.0),
                    model_metadata={},
                )

            # Build payload
            request_data = {
                "agentId": agent_id,
                "customerId": customer_id,
                "indicator": indicator,
                "rawUsage": {
                    "provider": raw_usage.provider,
                    "model": raw_usage.model,
                    "plan": raw_usage.plan or "default",
                },
            }
            if raw_usage.input_tokens is not None:
                request_data["rawUsage"]["inputTokens"] = raw_usage.input_tokens
            if raw_usage.output_tokens is not None:
                request_data["rawUsage"]["outputTokens"] = raw_usage.output_tokens
            if raw_usage.cached_tokens is not None:
                request_data["rawUsage"]["cachedTokens"] = raw_usage.cached_tokens
            if raw_usage.audio_duration is not None:
                request_data["rawUsage"]["audioDuration"] = raw_usage.audio_duration
            if raw_usage.character_count is not None:
                request_data["rawUsage"]["characterCount"] = raw_usage.character_count

            # Video-specific fields
            if raw_usage.video_duration_sec is not None:
                request_data["rawUsage"]["videoDurationSec"] = raw_usage.video_duration_sec
            if raw_usage.video_resolution is not None:
                request_data["rawUsage"]["videoResolution"] = raw_usage.video_resolution
            if raw_usage.video_is_audio is not None:
                request_data["rawUsage"]["videoIsAudio"] = raw_usage.video_is_audio

            url = urljoin(self.base_url, "/api/v2/usage")
            headers = {
                "Content-Type": "application/json",
                "paygent-api-key": self.api_key,
            }

            def _fire():
                try:
                    resp = self.session.post(
                        url,
                        json=request_data,
                        headers=headers,
                        timeout=10,  # 10-second total timeout per request
                    )
                    if resp.status_code >= 400:
                        self.logger.error(f"[Paygent] API error {resp.status_code}: {resp.text[:200]}")
                except requests.exceptions.Timeout:
                    self.logger.error(
                        "[Paygent] send_usage_v2 timed out — "
                        "Paygent server may be slow or unreachable."
                    )
                except requests.exceptions.ConnectionError:
                    self.logger.error(
                        "[Paygent] send_usage_v2 connection error — "
                        "could not reach Paygent server."
                    )
                except Exception as e:
                    self.logger.error(f"[Paygent] background dispatch error: {e}")

            self._executor.submit(_fire)

        except Exception as e:
            try:
                self.logger.error(f"[Paygent] send_usage_v2 error: {e}")
            except Exception:
                pass

        # Return placeholder immediately; actual response is in background
        return SendUsageV2Response(
            cp_data_id="",
            calculated_cost=0.0,
            breakdown=CostBreakdown(0.0, 0.0, 0.0, 0.0, 0.0),
            model_metadata={},
        )

    def send_usage_video(
        self,
        agent_id: str,
        customer_id: str,
        indicator: str,
        video_usage: VideoUsageData
    ) -> SendUsageV2Response:
        """
        Send video generation usage data to the Paygent API.

        This method is fire-and-forget: it submits the usage data to a
        background thread and returns immediately.  It never blocks and never
        raises an exception.

        Video pricing is based on duration (per-second pricing) and whether
        the video includes audio.  The API automatically selects the correct
        pricing tier based on the video_is_audio flag.

        Args:
            agent_id: Unique identifier for the agent
            customer_id: Unique identifier for the customer
            indicator: Usage indicator string (e.g. "video_generation")
            video_usage: Video generation usage data

        Returns:
            SendUsageV2Response containing cost breakdown and cp_data_id
        """
        default_response = SendUsageV2Response(
            cp_data_id="",
            calculated_cost=0.0,
            breakdown=CostBreakdown(0.0, 0.0, 0.0, 0.0, 0.0),
            model_metadata={},
        )
        try:
            if not agent_id or not customer_id or not indicator:
                self.logger.warning(
                    "[Paygent] send_usage_video skipped: agent_id, customer_id, "
                    "and indicator are all required."
                )
                return default_response

            if video_usage.video_duration_sec is None or video_usage.video_duration_sec <= 0:
                self.logger.warning(
                    "[Paygent] send_usage_video skipped: video_duration_sec must be a positive number."
                )
                return default_response

            raw_usage = RawUsageData(
                provider=video_usage.provider,
                model=video_usage.model,
                plan=video_usage.plan or "default",
                video_duration_sec=video_usage.video_duration_sec,
                video_resolution=video_usage.video_resolution,
                video_is_audio=video_usage.video_is_audio
            )

            return self.send_usage_v2(agent_id, customer_id, indicator, raw_usage)

        except Exception as e:
            try:
                self.logger.error(f"[Paygent] send_usage_video error: {e}")
            except Exception:
                pass
            return default_response

    # ------------------------------------------------------------------
    # Customer management — fully async, 1-second timeout
    # ------------------------------------------------------------------

    def create_or_get_customer(self, request: CustomerCreateOrGetRequest) -> Customer:
        """
        Idempotently create or retrieve a customer — fire-and-forget.

        - customer_name is optional; when absent, customer_id is used as name.
        - Hard timeout of 1 second on the HTTP call.
        - If the server is slow, down, or returns an error the failure is
          absorbed silently; a placeholder Customer is returned instantly.
        - This method NEVER raises an exception.
        """
        _placeholder_customer = Customer(id="", external_id="", name="")
        try:
            if request is None:
                self.logger.warning("[Paygent] create_or_get_customer skipped: request is None.")
                return _placeholder_customer

            # ---- Validate / normalise ----
            external_id = (getattr(request, "external_id", None) or "").strip()
            name = (getattr(request, "name", None) or "").strip()

            if not external_id:
                self.logger.warning(
                    "[Paygent] create_or_get_customer skipped: "
                    "customer_id (external_id) is required."
                )
                return Customer(id="", external_id="", name="")

            # Use customer_id as name when name is absent
            if not name:
                name = external_id

            # Normalise request in-place so the closure captures clean values
            request.external_id = external_id
            request.name = name

            # ---- Build payload ----
            payload: dict = {
                "name": name,
                "externalId": external_id,
            }
            if getattr(request, "email", None):
                payload["email"] = request.email
            if getattr(request, "website", None):
                payload["website"] = request.website
            if getattr(request, "phone", None):
                payload["phone"] = request.phone
            if getattr(request, "address_line1", None):
                payload["addressLine1"] = request.address_line1
            if getattr(request, "address_line2", None):
                payload["addressLine2"] = request.address_line2
            if getattr(request, "city", None):
                payload["city"] = request.city
            if getattr(request, "state", None):
                payload["state"] = request.state
            if getattr(request, "zip_code", None):
                payload["zipCode"] = request.zip_code
            if getattr(request, "country", None):
                payload["country"] = request.country

            url = urljoin(self.base_url, "/api/v1/customers/create-or-get")
            headers = {
                "Content-Type": "application/json",
                "paygent-api-key": self.api_key,
            }

            def _fire():
                try:
                    resp = self.session.post(
                        url,
                        json=payload,
                        headers=headers,
                        timeout=_CUSTOMER_TIMEOUT_SECONDS,  # ← hard 1-second limit
                    )
                    if resp.status_code >= 400:
                        self.logger.error(f"[Paygent] Customer create API error {resp.status_code}: {resp.text[:200]}")
                except requests.exceptions.Timeout:
                    self.logger.error(
                        "[Paygent] create_or_get_customer timed out after "
                        f"{_CUSTOMER_TIMEOUT_SECONDS}s — call dropped silently."
                    )
                except requests.exceptions.ConnectionError:
                    self.logger.error(
                        "[Paygent] create_or_get_customer connection error — "
                        "Paygent server unreachable, call dropped silently."
                    )
                except Exception as e:
                    self.logger.error(f"[Paygent] Customer create background error: {e}")

            self._executor.submit(_fire)

        except Exception as e:
            try:
                self.logger.error(f"[Paygent] create_or_get_customer error: {e}")
            except Exception:
                pass

        # Return a shell Customer immediately so the caller's code doesn't break
        return Customer(
            id="",
            external_id=getattr(request, "external_id", ""),
            name=getattr(request, "name", ""),
        )

    # ------------------------------------------------------------------
    # Token counting — internal helpers
    # ------------------------------------------------------------------

    def _get_token_count(self, model: str, text: str) -> int:
        """Count tokens for the given model/text using tiktoken (with fallback)."""
        try:
            if not text:
                return 0
            if not tiktoken:
                return self._fallback_token_count(text)

            model_lower = (model or "").lower()
            try:
                if model_lower.startswith("gpt-"):
                    enc = tiktoken.encoding_for_model(model)
                else:
                    enc = tiktoken.get_encoding("cl100k_base")
                return len(enc.encode(text))
            except Exception:
                return self._fallback_token_count(text)
        except Exception:
            return 0

    def _fallback_token_count(self, text: str) -> int:
        """Word-count heuristic: ~1.3 tokens per word."""
        try:
            return int(len(text.split()) * 1.3)
        except Exception:
            return 0
