"""
JPMorgan Nexus API Client for Portfolio Rebalancing and Reporting.

This class provides a secure, cached interface to the JPM Nexus Investable Indices API,
handling OAuth2 client credentials flow, mTLS-ready headers (extend for certs), and common
operations like rebalance submission, status polling, and position reports. Designed for
Django production with Redis caching and settings integration.

Key Features:
- Automatic OAuth token refresh with expiry-aware caching.
- Virtual ISIN fallback for testing/debug.
- Robust error handling with JPM-specific status validation.
- Timeout-configured requests for reliability.

Usage:
    client = JPMNexusAPIClient(settings.JPM_NEXUS_CLIENT_ID, settings.JPM_NEXUS_CLIENT_SECRET)
    result = client.submit_rebalance('US1234567890', date(2026, 1, 30), holdings)
"""

from datetime import date
from json import JSONDecodeError
from typing import Any

import requests
from django.conf import settings
from django.core.cache import cache
from requests import HTTPError
from wbcore.utils.jwt import is_expired


class JPMNexusAPIClient:
    """Client for JPMorgan Nexus Investable Indices API."""

    # API Configuration
    BASE_URL = "https://api-developer.jpmorgan.com/investableindices"

    # Response & Testing Constants
    SUCCESS_VALUE = "success"
    VIRTUAL_AMC_ISIN = "JPLSDEMO"  # Sandbox ISIN for testing/debug

    # Caching (Redis-backed via Django)
    JPM_TOKEN_CACHE_KEY = "jpm_nexus_oauth_token"  # noqa: S105

    def __init__(self, client_id: str, client_secret: str) -> None:
        """
        Initialize the Nexus API client.

        Args:
            client_id: OAuth client ID from JPM developer portal.
            client_secret: Corresponding client secret (keep secure!).
        """
        self.client_id = client_id
        self.client_secret = client_secret

    @property
    def oauth_token(self) -> str:
        """Get valid OAuth token, refreshing from cache or API if expired."""
        token = cache.get(self.JPM_TOKEN_CACHE_KEY)
        if not token or is_expired(token):
            token, expires_in = self._get_token()
            cache.set(self.JPM_TOKEN_CACHE_KEY, token, expires_in - 60)  # Buffer
        return token

    @property
    def headers(self) -> dict[str, str]:
        """HTTP headers with dynamic Bearer token and content type."""
        return {"Content-Type": "application/x-www-form-urlencoded", "Authorization": f"Bearer {self.oauth_token}"}

    def _get_token(self) -> tuple[str, int]:
        """Fetch fresh OAuth2 access token using client credentials grant."""
        data = {
            "grant_type": "client_credentials",
            "client_id": self.client_id,  # Use instance vars, not settings here
            "client_secret": self.client_secret,
            "aud": "JPMC:URI:RS-69387-NexusAuthE-PROD",
        }
        response = requests.post(
            "https://authe.jpmorgan.com/as/token.oauth2",
            headers={"Content-Type": "application/x-www-form-urlencoded"},
            data=data,
            timeout=30,
        )
        response.raise_for_status()
        payload = response.json()
        return payload["access_token"], int(payload["expires_in"])

    def _get_json(self, response: requests.Response) -> dict[str, Any]:
        """Safely parse JSON response, returning empty dict on failure."""
        try:
            return response.json()
        except JSONDecodeError:
            return {}

    def _validate_ticker(self, ticker: str, test: bool = False) -> str:
        """Normalize ISIN for rebalancing; use virtual sandbox in test/debug mode."""
        if test or settings.DEBUG:
            return self.VIRTUAL_AMC_ISIN
        return ticker

    def _raise_for_status(self, response: requests.Response) -> None:
        """
        Raise HTTPError on non-2xx or JPM-specific 'error' in payload.

        JPM returns 200 with error details in some failure cases.
        """
        json_data = self._get_json(response)
        error = json_data.get("error")
        if response.status_code != 200 or error:
            raise HTTPError(f"JPM Nexus error {response.status_code}: {error}", response=response)

    def is_healthy(self) -> bool:
        """
        Health check: Verify token validity via JPM health endpoint.

        Returns:
            True if API accessible and token valid.
        """
        response = requests.get(
            f"{self.BASE_URL}/healthcheck",
            headers={"Authorization": f"Bearer {self.oauth_token}"},
            timeout=30,
        )
        return response.status_code == 200 and response.text == "Healthy"

    def submit_rebalance(
        self,
        ticker: str,
        rebalance_date: date,
        holdings: list[dict[str, str]],
        test: bool = False,
        rebalance_type: str = "ClientAdHoc",
        identifier_type: str = "BloombergTicker",
        comment: str | None = None,
    ) -> dict[str, Any]:
        """
        Submit portfolio rebalance request to JPM Nexus.

        Args:
            ticker: AMC/Portfolio ISIN (virtual in test mode).
            rebalance_date: Effective date (MM-DD-YYYY format).
            holdings: list of {'identifier': str, 'targetWeight': float} dicts.
            test: Use virtual ISIN if True.
            rebalance_type: e.g., 'ClientAdHoc'.
            identifier_type: e.g., 'BloombergTicker'.
            comment: Optional free-text comment.

        Returns:
            Parsed JSON result dict.

        Raises:
            HTTPError: On API failure or invalid response.
        """
        ticker = self._validate_ticker(ticker, test=test)
        url = f"{self.BASE_URL}/v3/rebalance/default/{ticker}"
        payload: dict[str, Any] = {
            "RebalanceType": rebalance_type,
            "IdentifierType": identifier_type,
            "RebalanceDate": rebalance_date.strftime("%m-%d-%Y"),
            "Holdings": holdings,
        }
        if comment:
            payload["Comments"] = comment

        response = requests.post(url, json=payload, headers=self.headers, timeout=60)
        self._raise_for_status(response)
        return self._get_json(response).get("result", {})

    def get_rebalance_status_for_ticker(self, ticker: str, rebalance_date: date) -> dict[str, Any]:
        """
        Fetch rebalance request status for specific ISIN/date.

        Args:
            ticker: Portfolio ISIN.
            rebalance_date: Rebalance date.

        Returns:
            Status details dict.
        """
        url = (
            f"{self.BASE_URL}/v1/report/rebalancerequests/{ticker}?rebalanceDate={rebalance_date.strftime('%m-%d-%Y')}"
        )
        response = requests.get(url, headers=self.headers, timeout=30)
        self._raise_for_status(response)
        return self._get_json(response)

    def get_current_rebalance_request(self, ticker: str, rebalance_id: str, rebalance_type: str) -> dict[str, Any]:
        """
        Returns:
            Status details dict.
        """
        url = f"{self.BASE_URL}/v3/rebalance/status/{ticker}?{rebalance_id}&type={rebalance_type}"
        response = requests.get(url, headers=self.headers, timeout=30)
        self._raise_for_status(response)
        return self._get_json(response)

    def get_rebalance_reports(self, ticker: str, rebalance_date: date) -> dict[str, Any]:
        """Fetch detailed rebalance reports for ISIN/date."""
        url = (
            f"{self.BASE_URL}/v1/report/rebalancedetails/{ticker}?rebalanceDate={rebalance_date.strftime('%m-%d-%Y')}"
        )
        response = requests.get(url, headers=self.headers, timeout=30)
        self._raise_for_status(response)
        return self._get_json(response)

    def cancel_rebalance(self, ticker: str) -> dict[str, Any]:
        """Cancel pending rebalance request (TODO: implement endpoint)."""
        raise NotImplementedError("Cancellation endpoint not yet mapped.")

    def get_portfolio_at_date(self, ticker: str, report_date: date) -> dict[str, Any]:
        """
        Retrieve portfolio positions at specific date.

        Args:
            ticker: Portfolio ISIN.
            report_date: Report date (DD-MM-YYYY format per API).

        Returns:
            Position data dict.
        """
        url = f"{self.BASE_URL}/v1/nexusreport/positiondata/{ticker}?reportDate={report_date.strftime('%m-%d-%Y')}"
        response = requests.get(url, headers=self.headers, timeout=10)
        self._raise_for_status(response)
        return self._get_json(response)

    def get_amc_report(self, report_type: str, ticker: str, report_date: date) -> dict[str, Any]:
        """
        Retrieve amc report at specific date.

        Args:
            report_type: ca | positionUnits | fees |
            ticker: Portfolio ISIN.
            report_date: Report date (DD-MM-YYYY format per API).

        Returns:
            Position data dict.
        """
        url = f"{self.BASE_URL}/v1/amc/{report_type}/{ticker}?reportIdentifier=default&reportDate={report_date.strftime('%m-%d-%Y')}"
        response = requests.get(url, headers=self.headers, timeout=10)
        self._raise_for_status(response)
        return self._get_json(response)

    def get_management_fees(self, ticker: str, from_date: date, to_date: date) -> dict[str, Any]:
        """Fetch management fees (TODO: implement)."""
        raise NotImplementedError("Management fees endpoint pending.")

    def get_performance_fees(self, ticker: str, from_date: date, to_date: date) -> dict[str, Any]:
        """Fetch performance fees (TODO: implement)."""
        raise NotImplementedError("Performance fees endpoint pending.")

    def validate_response(self, response: dict[str, Any]) -> dict[str, Any]:
        """
        Validate API response status.

        Args:
            response: Raw API response dict.

        Returns:
            Response if 'status' == 'SUCCESS', else empty dict.
        """
        if response.get("success") is True:
            return response
        return {}
