"""
Risk Assessment Model Management Functions

Simple functions for managing risk assessment models via the API.
"""

import requests
import json
from typing import Optional, Dict, Any, List, BinaryIO
from pathlib import Path

from .config import get_base_path, get_headers
from .exceptions import APIError


def _handle_response(response: requests.Response) -> Dict[str, Any]:
    """Handle API response and raise appropriate errors."""
    try:
        response.raise_for_status()
        # Handle 204 No Content responses
        if response.status_code == 204:
            return {}
        # Handle empty responses
        if not response.content:
            return {}
        return response.json()
    except requests.exceptions.HTTPError as e:
        error_msg = "Unknown error"
        try:
            error_data = response.json()
            error_msg = error_data.get('error', str(e))
        except:
            error_msg = response.text or str(e)
        raise APIError(f"API request failed: {error_msg}", response.status_code)
    except requests.exceptions.JSONDecodeError:
        raise APIError(f"Invalid JSON response: {response.text}", response.status_code)
    except requests.exceptions.RequestException as e:
        raise APIError(f"Request failed: {str(e)}", None)


def list_models() -> List[Dict[str, Any]]:
    """
    Get all models.
    
    Returns:
        List of model dictionaries
    
    Raises:
        APIError: If the API request fails
        ValueError: If configuration is not set
    """
    url = f"{get_base_path()}/get-all-models/"
    response = requests.get(url, headers=get_headers())
    return _handle_response(response)


def get_model(model_identifier: Dict[str, str]) -> Dict[str, Any]:
    """
    Get a single model by name, category, and version.
    
    Args:
        model_identifier: Dictionary containing:
            - name: Model name
            - category: Model category
            - version: Model version
    
    Returns:
        Model dictionary
    
    Raises:
        APIError: If the API request fails or model not found
        ValueError: If configuration is not set or required keys missing
    """
    required_keys = ['name', 'category', 'version']
    missing_keys = [key for key in required_keys if key not in model_identifier]
    if missing_keys:
        raise ValueError(f"Missing required keys in model_identifier: {missing_keys}")
    
    url = f"{get_base_path()}/get-single-model/"
    params = {
        'name': model_identifier['name'],
        'category': model_identifier['category'],
        'version': model_identifier['version']
    }
    response = requests.get(url, headers=get_headers(), params=params)
    return _handle_response(response)


def create_model(model_data: Dict[str, Any]) -> Dict[str, Any]:
    """
    Create a new model.
    
    Args:
        model_data: Dictionary containing:
            - name: Model name (required)
            - category: Model category (required)
            - version: Model version (required)
            - description: Model description (optional, default: "Latest model")
            - metrics: Model metrics dictionary (optional, will be JSON serialized)
            - other_training_results: Other training results dictionary (optional, will be JSON serialized)
            - model_file: File-like object for the model file (optional)
            - model_file_path: Path to model file (optional, alternative to model_file)
    
    Returns:
        Empty dict on success (201 Created)
    
    Raises:
        APIError: If the API request fails
        ValueError: If both model_file and model_file_path are provided, or configuration is not set, or required keys missing
        FileNotFoundError: If model_file_path is provided but file doesn't exist
    """
    required_keys = ['name', 'category', 'version']
    missing_keys = [key for key in required_keys if key not in model_data]
    if missing_keys:
        raise ValueError(f"Missing required keys in model_data: {missing_keys}")
    
    url = f"{get_base_path()}/create-model/"
    
    # Prepare payload
    payload = {
        'name': model_data['name'],
        'category': model_data['category'],
        'version': model_data['version'],
        'description': model_data.get('description', 'Latest model'),
        'metrics': json.dumps(model_data.get('metrics', {})),
    }
    
    if 'other_training_results' in model_data:
        payload['other_training_results'] = json.dumps(model_data['other_training_results'])
    
    # Handle file upload
    files = None
    if 'model_file' in model_data and 'model_file_path' in model_data:
        raise ValueError("Cannot specify both model_file and model_file_path")
    
    if 'model_file_path' in model_data:
        file_path = Path(model_data['model_file_path'])
        if not file_path.exists():
            raise FileNotFoundError(f"Model file not found: {model_data['model_file_path']}")
        files = {
            'file': (file_path.name, open(file_path, 'rb'), 'application/octet-stream')
        }
    elif 'model_file' in model_data:
        # Try to get filename from file object
        model_file = model_data['model_file']
        filename = getattr(model_file, 'name', 'model.pkl')
        files = {
            'file': (filename, model_file, 'application/octet-stream')
        }
    
    response = requests.post(url, headers=get_headers(), data=payload, files=files)
    
    # Close file if we opened it
    if 'model_file_path' in model_data and files:
        files['file'][1].close()
    
    return _handle_response(response)


def update_model(model_identifier: Dict[str, str], model_data: Dict[str, Any]) -> Dict[str, Any]:
    """
    Update an existing model.
    
    Args:
        model_identifier: Dictionary containing:
            - name: Current model name (for identification)
            - category: Current model category (for identification)
            - version: Current model version (for identification)
        model_data: Dictionary containing optional update fields:
            - name: New model name
            - category: New model category
            - version: New model version
            - description: New description
            - metrics: Updated metrics dictionary (will be JSON serialized)
            - other_training_results: Updated training results dictionary (will be JSON serialized)
            - model_file: File-like object for the model file
            - model_file_path: Path to model file
    
    Returns:
        Updated model dictionary
    
    Raises:
        APIError: If the API request fails
        ValueError: If both model_file and model_file_path are provided, or configuration is not set, or required keys missing
        FileNotFoundError: If model_file_path is provided but file doesn't exist
    """
    required_keys = ['name', 'category', 'version']
    missing_keys = [key for key in required_keys if key not in model_identifier]
    if missing_keys:
        raise ValueError(f"Missing required keys in model_identifier: {missing_keys}")
    
    url = f"{get_base_path()}/update-model/"
    
    # Query parameters for identifying the model
    params = {
        'name': model_identifier['name'],
        'category': model_identifier['category'],
        'version': model_identifier['version']
    }
    
    # Prepare payload with only provided fields
    payload = {}
    if 'name' in model_data:
        payload['name'] = model_data['name']
    if 'category' in model_data:
        payload['category'] = model_data['category']
    if 'version' in model_data:
        payload['version'] = model_data['version']
    if 'description' in model_data:
        payload['description'] = model_data['description']
    if 'metrics' in model_data:
        payload['metrics'] = json.dumps(model_data['metrics'])
    if 'other_training_results' in model_data:
        payload['other_training_results'] = json.dumps(model_data['other_training_results'])
    
    # Handle file upload
    files = None
    if 'model_file' in model_data and 'model_file_path' in model_data:
        raise ValueError("Cannot specify both model_file and model_file_path")
    
    if 'model_file_path' in model_data:
        file_path = Path(model_data['model_file_path'])
        if not file_path.exists():
            raise FileNotFoundError(f"Model file not found: {model_data['model_file_path']}")
        files = {
            'file': (file_path.name, open(file_path, 'rb'), 'application/octet-stream')
        }
    elif 'model_file' in model_data:
        model_file = model_data['model_file']
        filename = getattr(model_file, 'name', 'model.pkl')
        files = {
            'file': (filename, model_file, 'application/octet-stream')
        }
    
    response = requests.put(url, headers=get_headers(), params=params, data=payload, files=files)
    
    # Close file if we opened it
    if 'model_file_path' in model_data and files:
        files['file'][1].close()
    
    return _handle_response(response)


def delete_model(model_identifier: Dict[str, str]) -> None:
    """
    Delete a model.
    
    Args:
        model_identifier: Dictionary containing:
            - name: Model name
            - category: Model category
            - version: Model version
    
    Raises:
        APIError: If the API request fails or model not found
        ValueError: If configuration is not set or required keys missing
    """
    required_keys = ['name', 'category', 'version']
    missing_keys = [key for key in required_keys if key not in model_identifier]
    if missing_keys:
        raise ValueError(f"Missing required keys in model_identifier: {missing_keys}")
    
    url = f"{get_base_path()}/delete-model/"
    params = {
        'name': model_identifier['name'],
        'category': model_identifier['category'],
        'version': model_identifier['version']
    }
    response = requests.delete(url, headers=get_headers(), params=params)
    _handle_response(response)
