"""Async OnStar client that uses the GM Auth API."""
from __future__ import annotations

import logging
import time
from typing import Any, Dict, List, Optional, cast

import httpx
from .api import OnStarAPIClient
from .auth import GMAuth, get_gm_api_jwt, DecodedPayload
from .commands import CommandFactory
from .types import (
    AlertRequestOptions,
    ChargeOverrideOptions,
    DiagnosticsRequestOptions,
    DoorRequestOptions,
    SetChargingProfileRequestOptions,
    TrunkRequestOptions,
)

__all__ = ["OnStar"]

TOKEN_REFRESH_WINDOW_SECONDS = 5 * 60
logger = logging.getLogger(__name__)


class OnStar:
    """Simple async OnStar client.

    Parameters
    ----------
    username
        GM / OnStar account email.
    password
        Account password.
    device_id
        UUID used by the official app (can be random UUID4).
    vin
        Vehicle VIN – will be upper-cased automatically.
    onstar_pin
        Numeral account PIN (currently *unused* but kept for future endpoints).
    totp_secret
        16-char secret used for MFA (Third-party authenticator).
    token_location
        Directory where ``microsoft_tokens.json`` / ``gm_tokens.json`` will be
        cached (default: current working directory).
    check_request_status
        When *True* follows up on command requests until they complete (default: True).
    request_polling_timeout_seconds
        Maximum time in seconds to poll for command status (default: 90).
    request_polling_interval_seconds
        Time in seconds to wait between status polling requests (default: 6).
    debug
        When *True* emits verbose debug output from both *GMAuth* and the high-
        level client.
    http_client
        Pre-configured httpx AsyncClient to use (if None, a new one will be created).
        This is useful for integrations that need to handle SSL certificate loading in
        a specific way, like Home Assistant.
    """

    def __init__(
        self,
        *,
        username: str,
        password: str,
        device_id: str,
        vin: str,
        onstar_pin: str,
        totp_secret: str,
        token_location: str | None = None,
        check_request_status: bool = True,
        request_polling_timeout_seconds: int = 90,
        request_polling_interval_seconds: int = 15,
        debug: bool = False,
        http_client: httpx.AsyncClient = None,
    ) -> None:
        """Initialize OnStar client."""
        self._vin = vin.upper()
        self._setup_logging(debug)
        self._auth = self._create_auth(
            username=username,
            password=password,
            device_id=device_id,
            totp_secret=totp_secret,
            token_location=token_location or "./",
            debug=debug,
        )
        
        # Set up API client
        self._api_client = OnStarAPIClient(
            request_polling_timeout_seconds=request_polling_timeout_seconds,
            request_polling_interval_seconds=request_polling_interval_seconds,
            debug=debug,
            http_client=http_client,
        )
        
        # Command status tracking
        self._check_request_status = check_request_status
        
        # Token information
        self._token_resp: Optional[Dict[str, Any]] = None
        self._decoded_payload: Optional[DecodedPayload] = None
        
        # Store available commands
        self._available_commands: Dict[str, Dict[str, Any]] = {}
        self._vehicle_data: Optional[Dict[str, Any]] = None

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _setup_logging(self, debug: bool) -> None:
        """Initialize logging configuration."""
        if debug:
            # Initialize basic logging config if debug is enabled.
            # This will only have an effect the first time it's called.
            logging.basicConfig(
                level=logging.DEBUG, 
                format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
            )

    def _create_auth(
        self,
        *,
        username: str,
        password: str,
        device_id: str,
        totp_secret: str,
        token_location: str,
        debug: bool,
    ) -> GMAuth:
        """Create and configure GMAuth instance."""
        return GMAuth(
            {
                "username": username,
                "password": password,
                "device_id": device_id,
                "totp_key": totp_secret,
                "token_location": token_location,
            },
            debug=debug,
        )

    def _needs_token_refresh(self, token: Dict[str, Any] | None) -> bool:
        """Check if the token needs to be refreshed."""
        if not token:
            return True
        return token.get("expires_at", 0) < int(time.time()) + TOKEN_REFRESH_WINDOW_SECONDS

    async def _ensure_token(self) -> None:
        """Ensure a valid token is available, refreshing if necessary."""
        if self._needs_token_refresh(self._token_resp):
            logger.debug("Retrieving new GM auth token…")
            # Use the async get_gm_api_jwt function
            res = await get_gm_api_jwt(
                self._auth.config,  # Pass GMAuth's config directly
                self._auth.config.get("debug", False), 
            )
            self._token_resp = cast(Dict[str, Any], res["token"])
            self._decoded_payload = cast(DecodedPayload, res["decoded_payload"])

    def _get_command_url(self, command_name: str) -> str:
        """Get the URL for a specific command from the available commands."""
        if command_name in self._available_commands:
            return self._available_commands[command_name]["url"]
        
        # Fallback to hardcoded paths if command not found
        logger.warning(f"Command '{command_name}' not found in available commands, using fallback URL")
        return f"/account/vehicles/{self._vin}/commands/{command_name}"

    async def _api_request(
        self, 
        method: str,
        path: str, 
        *, 
        json_body: Any | None = None,
        max_retries: int = 1,
        check_request_status: bool | None = None,
        max_polls: int | None = None
    ) -> Dict[str, Any]:
        """Make an authenticated request to the OnStar API."""
        await self._ensure_token()
        
        should_check_status = check_request_status if check_request_status is not None else self._check_request_status
        
        return await self._api_client.api_request(
            self._token_resp["access_token"],
            method,
            path,
            json_body=json_body,
            max_retries=max_retries,
            check_request_status=should_check_status,
            max_polls=max_polls
        )

    # ------------------------------------------------------------------
    # Public API methods
    # ------------------------------------------------------------------

    async def get_account_vehicles(self) -> Dict[str, Any]:
        """Get all vehicles associated with the account."""
        response = await self._api_request(
            "GET", 
            "/account/vehicles?includeCommands=true&includeEntitlements=true&includeModules=true"
        )
        
        # Parse and store available commands for the current VIN
        if response and "vehicles" in response and "vehicle" in response["vehicles"]:
            for vehicle in response["vehicles"]["vehicle"]:
                if vehicle.get("vin") == self._vin and "commands" in vehicle and "command" in vehicle["commands"]:
                    # Store the full vehicle data
                    self._vehicle_data = vehicle
                    
                    # Process commands
                    commands = {}
                    for cmd in vehicle["commands"]["command"]:
                        if "name" in cmd and "url" in cmd:
                            commands[cmd["name"]] = cmd
                    
                    self._available_commands = commands
                    logger.debug(f"Stored {len(commands)} available commands for VIN {self._vin}")
        
        return response

    def is_command_available(self, command_name: str) -> bool:
        """Check if a specific command is available for the vehicle."""
        return command_name in self._available_commands
    
    def get_command_data(self, command_name: str) -> Dict[str, Any]:
        """Get additional data for a specific command."""
        if command_name in self._available_commands:
            return self._available_commands[command_name]
        return {}
        
    def requires_privileged_session(self, command_name: str) -> bool:
        """Check if a command requires a privileged session."""
        if command_name in self._available_commands:
            return self._available_commands[command_name].get("isPrivSessionRequired", "false") == "true"
        return False

    async def execute_command(self, command_name: str, request_body: Dict[str, Any] = None) -> Dict[str, Any]:
        """Execute any available command discovered from the API.
        
        This generic method allows executing any command available for the vehicle,
        even if not explicitly implemented as a method in this client.
        
        Parameters
        ----------
        command_name
            The name of the command to execute (must be in the available commands)
        request_body
            Optional JSON body to send with the request
            
        Returns
        -------
        Dict[str, Any]
            The command response
            
        Raises
        ------
        ValueError
            If the command is not available for this vehicle
        """
        if not self.is_command_available(command_name):
            logger.error(f"Command '{command_name}' not available for this vehicle")
            raise ValueError(f"Command '{command_name}' not available for this vehicle")
        
        return await self._api_request(
            "POST",
            self._get_command_url(command_name),
            json_body=request_body
        )

    # ------------------------------------------------------------------
    # Vehicle Commands
    # ------------------------------------------------------------------

    async def start(self) -> Dict[str, Any]:
        """Start the vehicle."""
        return await self.execute_command("start")

    async def cancel_start(self) -> Dict[str, Any]:
        """Cancel the start command."""
        return await self.execute_command("cancelStart")

    async def lock_door(self, options: Optional[DoorRequestOptions] = None) -> Dict[str, Any]:
        """Lock the vehicle doors."""
        return await self.execute_command("lockDoor", CommandFactory.lock_door(options))

    async def unlock_door(self, options: Optional[DoorRequestOptions] = None) -> Dict[str, Any]:
        """Unlock the vehicle doors."""
        return await self.execute_command("unlockDoor", CommandFactory.unlock_door(options))

    async def lock_trunk(self, options: Optional[TrunkRequestOptions] = None) -> Dict[str, Any]:
        """Lock the vehicle trunk."""
        return await self.execute_command("lockTrunk", CommandFactory.lock_trunk(options))

    async def unlock_trunk(self, options: Optional[TrunkRequestOptions] = None) -> Dict[str, Any]:
        """Unlock the vehicle trunk."""
        return await self.execute_command("unlockTrunk", CommandFactory.unlock_trunk(options))

    async def alert(self, options: Optional[AlertRequestOptions] = None) -> Dict[str, Any]:
        """Trigger the vehicle alert (honk/flash)."""
        return await self.execute_command("alert", CommandFactory.alert(options))

    async def cancel_alert(self) -> Dict[str, Any]:
        """Cancel the alert command."""
        return await self.execute_command("cancelAlert")

    async def charge_override(self, options: Optional[ChargeOverrideOptions] = None) -> Dict[str, Any]:
        """Override vehicle charging settings."""
        return await self.execute_command("chargeOverride", CommandFactory.charge_override(options))

    async def get_charging_profile(self) -> Dict[str, Any]:
        """Get the vehicle charging profile."""
        return await self.execute_command("getChargingProfile")

    async def set_charging_profile(self, options: Optional[SetChargingProfileRequestOptions] = None) -> Dict[str, Any]:
        """Set the vehicle charging profile."""
        return await self.execute_command(
            "setChargingProfile", 
            CommandFactory.set_charging_profile(options)
        )

    async def get_charger_power_level(self) -> Dict[str, Any]:
        """Get the vehicle's charger power level."""
        return await self.execute_command("getChargerPowerLevel")
        
    def get_supported_diagnostics(self) -> List[str]:
        """Get the list of diagnostic items supported by the vehicle."""
        supported_diagnostics = []
        command_data = self.get_command_data("diagnostics")
        
        if (command_data and "commandData" in command_data and 
                "supportedDiagnostics" in command_data["commandData"] and 
                "supportedDiagnostic" in command_data["commandData"]["supportedDiagnostics"]):
            supported_diagnostics = command_data["commandData"]["supportedDiagnostics"]["supportedDiagnostic"]
            
        return supported_diagnostics

    async def diagnostics(
        self, 
        options: Optional[DiagnosticsRequestOptions] = None, 
        timeout_seconds: int = 180, 
        max_polls: Optional[int] = None
    ) -> Dict[str, Any]:
        """Get diagnostic data from the vehicle."""
        if not self.is_command_available("diagnostics"):
            logger.error("Diagnostics command not available for this vehicle")
            raise ValueError("Diagnostics command not available for this vehicle")
            
        # Get supported diagnostics
        supported_diagnostics = self.get_supported_diagnostics()
        
        if not supported_diagnostics:
            logger.warning("No supported diagnostics found for this vehicle")
            
        # Process requested diagnostic items
        requested_items = None
        if options and "diagnostic_item" in options:
            requested_items = options["diagnostic_item"]
            
            # Validate that requested items are supported
            unsupported = [item for item in requested_items if item not in supported_diagnostics]
            if unsupported:
                logger.warning(f"Requested unsupported diagnostic items: {unsupported}")
                logger.warning(f"Supported items: {supported_diagnostics}")
                
            # Filter to only include supported items
            requested_items = [item for item in requested_items if item in supported_diagnostics]
            
            if not requested_items:
                logger.error("None of the requested diagnostic items are supported")
                raise ValueError("None of the requested diagnostic items are supported")
        
        # Build request body with CommandFactory
        diagnostic_items = requested_items if requested_items else supported_diagnostics
        body = CommandFactory.diagnostics(diagnostic_items)
        
        # Use extended timeout for diagnostics
        return await self._api_request(
            "POST",
            self._get_command_url("diagnostics"),
            json_body=body,
            check_request_status=True,
            max_polls=max_polls
        )
        
    async def location(self) -> Dict[str, Any]:
        """Get the vehicle's current location."""
        return await self.execute_command("location")

    def get_vehicle_data(self) -> Dict[str, Any]:
        """Get the full vehicle data that was retrieved during get_account_vehicles."""
        if not self._vehicle_data:
            logger.warning("Vehicle data not available. Call get_account_vehicles() first.")
            return {}
        return self._vehicle_data
    
    def get_entitlements(self) -> List[Dict[str, Any]]:
        """Get the list of entitlements for the vehicle."""
        if not self._vehicle_data or "entitlements" not in self._vehicle_data:
            logger.warning("Entitlements not available. Call get_account_vehicles() first.")
            return []
        
        if "entitlement" in self._vehicle_data["entitlements"]:
            return self._vehicle_data["entitlements"]["entitlement"]
        return []
    
    def is_entitled(self, entitlement_id: str) -> bool:
        """Check if the vehicle is entitled to a specific feature."""
        entitlements = self.get_entitlements()
        for entitlement in entitlements:
            if entitlement.get("id") == entitlement_id and entitlement.get("eligible") == "true":
                return True
        return False

    def get_supported_hvac_settings(self) -> Dict[str, Any]:
        """Get the supported HVAC settings for the vehicle."""
        if not self._available_commands or "setHvacSettings" not in self._available_commands:
            logger.warning("HVAC settings command not available. Call get_account_vehicles() first.")
            return {}
        
        hvac_command = self._available_commands["setHvacSettings"]
        if "commandData" in hvac_command and "supportedHvacData" in hvac_command["commandData"]:
            return hvac_command["commandData"]["supportedHvacData"]
        return {}
        
    async def set_hvac_settings(self, ac_mode: Optional[str] = None, heated_steering_wheel: Optional[bool] = None) -> Dict[str, Any]:
        """Set HVAC settings for the vehicle."""
        if not self.is_command_available("setHvacSettings"):
            logger.error("setHvacSettings command not available for this vehicle")
            raise ValueError("setHvacSettings command not available for this vehicle")
        
        # Get supported settings to validate inputs
        supported_settings = self.get_supported_hvac_settings()
        
        # Validate AC climate mode if provided
        if ac_mode is not None:
            supported_modes = []
            if "supportedAcClimateModeSettings" in supported_settings:
                supported_modes = supported_settings["supportedAcClimateModeSettings"].get("supportedAcClimateModeSetting", [])
            
            if supported_modes and ac_mode not in supported_modes:
                supported_str = ", ".join(supported_modes) if supported_modes else "none"
                logger.warning(f"Unsupported AC climate mode: {ac_mode}. Supported modes: {supported_str}")
        
        # Validate heated steering wheel if provided
        if heated_steering_wheel is not None:
            is_supported = supported_settings.get("heatedSteeringWheelSupported", "false") == "true"
            if not is_supported:
                logger.warning("Heated steering wheel not supported by this vehicle")
        
        # Create command payload and execute
        return await self.execute_command(
            "setHvacSettings", 
            CommandFactory.set_hvac_settings(ac_mode, heated_steering_wheel)
        )

    async def close(self):
        """Close the API client and release resources."""
        if hasattr(self, '_api_client'):
            await self._api_client.close() 