# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Billing Service - Central service for billing operations.

Handles:
- Balance checks before AI operations
- Cost recording after AI operations
- Provider permission checks via RBAC
- Price calculation with markup
"""

import logging
from typing import Dict, Any, List, Optional
from datetime import datetime

from modules.datamodels.datamodelUam import User
from modules.datamodels.datamodelBilling import (
    BillingModelEnum,
    BillingCheckResult,
    TransactionTypeEnum,
    ReferenceTypeEnum,
    BillingTransaction,
    BillingBalanceResponse,
    parseBillingModelFromStoredValue,
)
from modules.interfaces.interfaceDbBilling import getInterface as getBillingInterface

logger = logging.getLogger(__name__)

# Markup percentage for internal pricing (+50% für Infrastruktur und Platform Service + 50% für Währungsrisiko ==> Faktor 2.0)
BILLING_MARKUP_PERCENT = 400

# Singleton cache
_billingServices: Dict[str, "BillingService"] = {}


def getService(currentUser: User, mandateId: str, featureInstanceId: str = None, featureCode: str = None) -> "BillingService":
    """
    Factory function to get or create a BillingService instance.
    
    Args:
        currentUser: Current user object
        mandateId: Mandate ID for context
        featureInstanceId: Optional feature instance ID
        featureCode: Optional feature code (e.g., 'automation')
        
    Returns:
        BillingService instance
    """
    cacheKey = f"{currentUser.id}_{mandateId}_{featureInstanceId}"
    
    if cacheKey not in _billingServices:
        _billingServices[cacheKey] = BillingService(currentUser, mandateId, featureInstanceId, featureCode)
    else:
        _billingServices[cacheKey].setContext(currentUser, mandateId, featureInstanceId, featureCode)
    
    return _billingServices[cacheKey]


def _get_feature_code_from_context(context) -> Optional[str]:
    """Extract featureCode from ServiceCenterContext."""
    if context.workflow and hasattr(context.workflow, "feature") and context.workflow.feature:
        return getattr(context.workflow.feature, "code", None)
    return getattr(context.workflow, "featureCode", None) if context.workflow else None


class BillingService:
    """
    Central billing service for AI operations.
    
    Responsibilities:
    - Check balance before operations
    - Record usage costs
    - Apply pricing markup
    - Check provider permissions via RBAC
    
    Supports both service center (context, get_service) and legacy (user, mandateId, ...) initialization.
    """

    def __init__(self, context_or_user, mandateId=None, featureInstanceId=None, featureCode=None, get_service=None):
        """
        Initialize the billing service.
        
        Service center: (context, get_service) - resolver passes exactly these two args
        Legacy: (currentUser, mandateId, featureInstanceId, featureCode) from getService() factory
        """
        # Detect service center: second arg is callable (get_service)
        if mandateId is not None and callable(mandateId):
            ctx = context_or_user
            get_service = mandateId
            self.currentUser = ctx.user
            self.mandateId = ctx.mandate_id or ""
            self.featureInstanceId = ctx.feature_instance_id
            self.featureCode = _get_feature_code_from_context(ctx)
        elif get_service is not None and hasattr(context_or_user, "user"):
            ctx = context_or_user
            self.currentUser = ctx.user
            self.mandateId = ctx.mandate_id or ""
            self.featureInstanceId = ctx.feature_instance_id
            self.featureCode = _get_feature_code_from_context(ctx)
        else:
            self.currentUser = context_or_user
            self.mandateId = mandateId or ""
            self.featureInstanceId = featureInstanceId
            self.featureCode = featureCode

        self._billingInterface = getBillingInterface(self.currentUser, self.mandateId)
        self._settingsCache = None
    
    def setContext(
        self, 
        currentUser: User, 
        mandateId: str, 
        featureInstanceId: str = None,
        featureCode: str = None
    ):
        """Update service context."""
        self.currentUser = currentUser
        self.mandateId = mandateId
        self.featureInstanceId = featureInstanceId
        self.featureCode = featureCode
        self._billingInterface = getBillingInterface(currentUser, mandateId)
        self._settingsCache = None
    
    def _getSettings(self) -> Optional[Dict[str, Any]]:
        """Get billing settings with caching."""
        if self._settingsCache is None:
            self._settingsCache = self._billingInterface.getSettings(self.mandateId)
        return self._settingsCache
    
    # =========================================================================
    # Price Calculation
    # =========================================================================
    
    def calculatePriceWithMarkup(self, basePriceCHF: float) -> float:
        """
        Calculate final price with markup.
        
        The AICore plugins return prices in their original currency (USD).
        This method applies the configured markup percentage.
        
        Args:
            basePriceCHF: Base price from AI model (actually USD from provider)
            
        Returns:
            Final price in CHF with markup applied
        """
        if basePriceCHF <= 0:
            return 0.0
        
        # Apply markup (50% = multiply by 1.5)
        markup_multiplier = 1 + (BILLING_MARKUP_PERCENT / 100)
        return round(basePriceCHF * markup_multiplier, 6)
    
    # =========================================================================
    # Balance Operations
    # =========================================================================
    
    def checkBalance(self, estimatedCost: float = 0.0) -> BillingCheckResult:
        """
        Check if the current user/mandate has sufficient balance.

        Gate order:
        1. Subscription active? (fast, cached) — blocks AI if not
        2. Budget sufficient? (existing prepaid logic)
        
        Args:
            estimatedCost: Estimated cost of the operation (with markup applied)
            
        Returns:
            BillingCheckResult indicating if operation is allowed
        """
        subResult = self._checkSubscription()
        if subResult is not None:
            return subResult

        return self._billingInterface.checkBalance(
            self.mandateId,
            self.currentUser.id,
            estimatedCost
        )

    def _checkSubscription(self) -> Optional[BillingCheckResult]:
        """Return a failing BillingCheckResult if subscription is not active, else None."""
        try:
            from modules.datamodels.datamodelSubscription import SubscriptionStatusEnum
            from modules.serviceCenter.services.serviceSubscription.mainServiceSubscription import (
                getService as getSubscriptionService,
                _subscriptionReasonForStatus,
                _subscriptionUserActionForStatus,
            )

            subService = getSubscriptionService(self.currentUser, self.mandateId)
            status = subService.assertActive(self.mandateId)

            if status in (SubscriptionStatusEnum.ACTIVE, SubscriptionStatusEnum.TRIALING, SubscriptionStatusEnum.PAST_DUE):
                return None

            return BillingCheckResult(
                allowed=False,
                reason=_subscriptionReasonForStatus(status),
                upgradeRequired=True,
                subscriptionUiPath="/admin/billing?tab=subscription",
                userAction=_subscriptionUserActionForStatus(status),
            )
        except Exception as e:
            logger.warning(f"Subscription check failed (allowing): {e}")
            return None
    
    def hasBalance(self, estimatedCost: float = 0.0) -> bool:
        """
        Quick check if balance is sufficient.
        
        Args:
            estimatedCost: Estimated cost with markup
            
        Returns:
            True if operation is allowed
        """
        result = self.checkBalance(estimatedCost)
        return result.allowed
    
    def getCurrentBalance(self) -> float:
        """
        Get current balance for the user/mandate.
        
        Returns:
            Current balance in CHF
        """
        result = self.checkBalance(0.0)
        return result.currentBalance or 0.0
    
    # =========================================================================
    # Usage Recording
    # =========================================================================
    
    def recordUsage(
        self,
        priceCHF: float,
        workflowId: str = None,
        aicoreProvider: str = None,
        aicoreModel: str = None,
        description: str = None,
        processingTime: float = None,
        bytesSent: int = None,
        bytesReceived: int = None,
        errorCount: int = None
    ) -> Optional[Dict[str, Any]]:
        """Record AI usage cost as a billing transaction with markup applied."""
        if priceCHF <= 0:
            return None
        
        finalPrice = self.calculatePriceWithMarkup(priceCHF)
        if finalPrice <= 0:
            return None
        
        if not description:
            description = f"AI Usage: {aicoreModel or aicoreProvider or 'unknown'}"
        
        return self._billingInterface.recordUsage(
            mandateId=self.mandateId,
            userId=self.currentUser.id,
            priceCHF=finalPrice,
            workflowId=workflowId,
            featureInstanceId=self.featureInstanceId,
            featureCode=self.featureCode,
            aicoreProvider=aicoreProvider,
            aicoreModel=aicoreModel,
            description=description,
            processingTime=processingTime,
            bytesSent=bytesSent,
            bytesReceived=bytesReceived,
            errorCount=errorCount
        )
    
    def getWorkflowCost(self, workflowId: str) -> float:
        """Get total cost for a workflow from billing transactions."""
        return self._billingInterface.getWorkflowCost(workflowId)
    
    # =========================================================================
    # Provider Permission Check (via RBAC)
    # =========================================================================
    
    def isProviderAllowed(self, provider: str) -> bool:
        """
        Check if the user has permission to use an AICore provider.
        
        Uses RBAC to check for resource permission:
        resource.aicore.{provider}
        
        Args:
            provider: Provider name (e.g., 'anthropic', 'openai')
            
        Returns:
            True if provider is allowed
        """
        try:
            from modules.security.rbac import RbacClass
            from modules.datamodels.datamodelRbac import AccessRuleContext
            from modules.security.rootAccess import getRootDbAppConnector
            
            # Get database connector via established pattern
            dbApp = getRootDbAppConnector()
            
            rbac = RbacClass(dbApp, dbApp)
            resourceKey = f"resource.aicore.{provider}"
            
            # Check if user has view permission for this resource (view = use for RESOURCE context)
            permissions = rbac.getUserPermissions(
                self.currentUser,
                AccessRuleContext.RESOURCE,
                resourceKey,
                mandateId=self.mandateId
            )
            
            return permissions.view
        except Exception as e:
            logger.warning(f"Error checking provider permission: {e}")
            # Default to allowed if RBAC check fails
            return True
    
    def getallowedProviders(self) -> List[str]:
        """
        Get list of AICore providers the user is allowed to use.
        
        Returns:
            List of allowed provider names
        """
        try:
            from modules.aicore.aicoreModelRegistry import modelRegistry
            
            # Get all available providers
            connectors = modelRegistry.discoverConnectors()
            allProviders = [c.getConnectorType() for c in connectors]
            
            # Filter by RBAC permissions
            return [p for p in allProviders if self.isProviderAllowed(p)]
        except Exception as e:
            logger.warning(f"Error getting allowed providers: {e}")
            return []
    
    # =========================================================================
    # Admin Operations
    # =========================================================================
    
    def addCredit(
        self,
        amount: float,
        description: str = "Manual credit",
        referenceType: ReferenceTypeEnum = ReferenceTypeEnum.ADMIN
    ) -> Optional[Dict[str, Any]]:
        """
        Add credit to the account (admin operation).
        
        Args:
            amount: Amount to credit (positive)
            description: Transaction description
            referenceType: Reference type (ADMIN, PAYMENT, SYSTEM)
            
        Returns:
            Created transaction dict or None
        """
        if amount <= 0:
            return None
        
        settings = self._getSettings()
        if not settings:
            logger.warning(f"No billing settings for mandate {self.mandateId}")
            return None
        
        billingModel = parseBillingModelFromStoredValue(settings.get("billingModel"))
        
        # Get or create account
        if billingModel == BillingModelEnum.PREPAY_USER:
            account = self._billingInterface.getOrCreateUserAccount(
                self.mandateId, 
                self.currentUser.id,
                initialBalance=0.0
            )
        else:
            account = self._billingInterface.getOrCreateMandateAccount(
                self.mandateId,
                initialBalance=0.0
            )
        
        # Create credit transaction
        transaction = BillingTransaction(
            accountId=account["id"],
            transactionType=TransactionTypeEnum.CREDIT,
            amount=amount,
            description=description,
            referenceType=referenceType
        )
        
        return self._billingInterface.createTransaction(transaction)
    
    # =========================================================================
    # Statistics & Reporting
    # =========================================================================
    
    def getBalancesForUser(self) -> List[BillingBalanceResponse]:
        """
        Get all billing balances for the current user.
        
        Returns:
            List of balance responses for each mandate
        """
        return self._billingInterface.getBalancesForUser(self.currentUser.id)
    
    def getTransactionHistory(self, limit: int = 100) -> List[Dict[str, Any]]:
        """
        Get transaction history for the user across all mandates.
        
        Args:
            limit: Maximum number of transactions
            
        Returns:
            List of transactions
        """
        return self._billingInterface.getTransactionsForUser(self.currentUser.id, limit=limit)


# ============================================================================
# Exception Classes
# ============================================================================

BILLING_USER_ACTION_TOP_UP_SELF = "TOP_UP_SELF"
BILLING_USER_ACTION_CONTACT_MANDATE_ADMIN = "CONTACT_MANDATE_ADMIN"


def _userActionForBillingModel(bm: BillingModelEnum) -> str:
    if bm == BillingModelEnum.PREPAY_USER:
        return BILLING_USER_ACTION_TOP_UP_SELF
    return BILLING_USER_ACTION_CONTACT_MANDATE_ADMIN


def _buildInsufficientBalanceMessages(
    bm: BillingModelEnum,
    currentBalance: float,
    requiredAmount: float,
) -> tuple:
    bal_s = f"{currentBalance:.2f}"
    req_s = f"{requiredAmount:.2f}"
    if bm == BillingModelEnum.PREPAY_USER:
        msg_de = (
            f"Ihr persönliches Guthaben ist aufgebraucht (aktuell CHF {bal_s}, benötigt mindestens CHF {req_s}). "
            "Bitte laden Sie unter „Billing“ Guthaben nach."
        )
        msg_en = (
            f"Your personal balance is exhausted (current CHF {bal_s}, at least CHF {req_s} required). "
            "Please top up under Billing."
        )
    else:
        msg_de = (
            f"Das Mandanten-Budget ist aufgebraucht (aktuell CHF {bal_s}, benötigt mindestens CHF {req_s}). "
            "Bitte informieren Sie die Administratorin bzw. den Administrator Ihres Mandanten. "
            "Die in den Billing-Einstellungen hinterlegten Kontakte wurden per E-Mail informiert (falls konfiguriert)."
        )
        msg_en = (
            f"The organization budget is exhausted (current CHF {bal_s}, at least CHF {req_s} required). "
            "Please contact your mandate administrator. Billing notification contacts were emailed if configured."
        )
    return msg_de, msg_en


class InsufficientBalanceException(Exception):
    """Raised when there's insufficient balance for an operation.

    Carries structured fields for API/SSE clients (userAction, billingModel, localized hints).
    """

    def __init__(
        self,
        currentBalance: float,
        requiredAmount: float,
        message: Optional[str] = None,
        *,
        billing_model: Optional[BillingModelEnum] = None,
        mandate_id: str = "",
        user_action: Optional[str] = None,
        message_de: Optional[str] = None,
        message_en: Optional[str] = None,
    ):
        self.currentBalance = float(currentBalance)
        self.requiredAmount = float(requiredAmount)
        self.billing_model = billing_model
        self.mandate_id = mandate_id or ""
        if billing_model is not None:
            self.user_action = user_action or _userActionForBillingModel(billing_model)
        else:
            self.user_action = user_action or BILLING_USER_ACTION_TOP_UP_SELF

        if message_de is not None and message_en is not None:
            self.message_de = message_de
            self.message_en = message_en
            self.message = message or message_de
        elif message:
            self.message = message
            self.message_de = message
            self.message_en = message
        else:
            bm = billing_model or BillingModelEnum.PREPAY_USER
            md, me = _buildInsufficientBalanceMessages(bm, self.currentBalance, self.requiredAmount)
            self.message_de = md
            self.message_en = me
            self.message = md
        super().__init__(self.message)

    @classmethod
    def fromBalanceCheck(
        cls,
        check: BillingCheckResult,
        mandate_id: str,
        required_amount: float,
    ) -> "InsufficientBalanceException":
        bm = check.billingModel or BillingModelEnum.PREPAY_MANDATE
        bal = float(check.currentBalance or 0.0)
        msg_de, msg_en = _buildInsufficientBalanceMessages(bm, bal, required_amount)
        return cls(
            bal,
            required_amount,
            message=msg_de,
            billing_model=bm,
            mandate_id=mandate_id or "",
            message_de=msg_de,
            message_en=msg_en,
        )

    def toClientDict(self) -> Dict[str, Any]:
        """Structured payload for HTTP 402, SSE item, or JSON error details."""
        out: Dict[str, Any] = {
            "error": "INSUFFICIENT_BALANCE",
            "currentBalance": round(self.currentBalance, 4),
            "requiredAmount": round(self.requiredAmount, 4),
            "message": self.message,
            "messageDe": self.message_de,
            "messageEn": self.message_en,
            "userAction": self.user_action,
        }
        if self.billing_model is not None:
            out["billingModel"] = self.billing_model.value
        if self.mandate_id:
            out["mandateId"] = self.mandate_id
        if self.user_action == BILLING_USER_ACTION_TOP_UP_SELF:
            out["billingUiPath"] = "/billing"
        return out


class ProviderNotAllowedException(Exception):
    """Raised when a user doesn't have permission to use an AI provider."""
    
    def __init__(self, provider: str, message: str = None):
        self.provider = provider
        self.message = message or f"Provider '{provider}' is not allowed for your role"
        super().__init__(self.message)


class BillingContextError(Exception):
    """Raised when billing context is incomplete (missing mandateId, user, etc.).
    
    This is a FAIL-SAFE error: AI calls MUST NOT proceed without valid billing context.
    Acts like a 0 CHF credit card pre-authorization check - validates that billing
    CAN be recorded before any expensive AI operation starts.
    """
    
    def __init__(self, message: str = None):
        self.message = message or "Billing context incomplete - AI call blocked"
        super().__init__(self.message)


# Expose exception classes on BillingService so consumers can use service.InsufficientBalanceException
# instead of importing from this module
BillingService.InsufficientBalanceException = InsufficientBalanceException
BillingService.ProviderNotAllowedException = ProviderNotAllowedException
BillingService.BillingContextError = BillingContextError
