# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""Accounting bridge: standardised interface between Trustee and external accounting systems.

Encapsulates: config loading -> connector resolution -> duplicate check -> push -> sync record.
"""

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

from .accountingConnectorBase import (
    AccountingBooking,
    AccountingBookingLine,
    AccountingChart,
    SyncResult,
)
from .accountingRegistry import _getAccountingRegistry

logger = logging.getLogger(__name__)


class AccountingBridge:
    """Routes accounting operations through the correct connector for a feature instance."""

    def __init__(self, trusteeInterface):
        self._trusteeInterface = trusteeInterface
        self._registry = _getAccountingRegistry()

    async def getActiveConfig(self, featureInstanceId: str) -> Optional[Dict[str, Any]]:
        """Load the active TrusteeAccountingConfig for a feature instance."""
        from modules.features.trustee.datamodelFeatureTrustee import TrusteeAccountingConfig
        records = self._trusteeInterface.db.getRecordset(
            TrusteeAccountingConfig,
            recordFilter={"featureInstanceId": featureInstanceId, "isActive": True},
        )
        if not records:
            return None
        record = records[0]
        return {k: v for k, v in record.items() if not k.startswith("_")}

    def _decryptConfig(self, encryptedConfig: str) -> Dict[str, Any]:
        """Decrypt the stored connector config JSON."""
        from modules.shared.configuration import decryptValue
        import json
        try:
            if not encryptedConfig:
                logger.error("Accounting config encryptedConfig is empty")
                return {}
            decrypted = decryptValue(encryptedConfig, keyName="accountingConfig")
            result = json.loads(decrypted) if isinstance(decrypted, str) else decrypted
            configKeys = list(result.keys()) if isinstance(result, dict) else []
            logger.info(f"Decrypted accounting config successfully, keys: {configKeys}")
            return result
        except Exception as e:
            logger.error(f"Failed to decrypt accounting config: {e}")
            return {}

    async def _resolveConnectorAndConfig(self, featureInstanceId: str):
        """Load config, decrypt, resolve connector. Returns (connector, plainConfig, accountingConfigRecord)."""
        configRecord = await self.getActiveConfig(featureInstanceId)
        if not configRecord:
            logger.warning(f"No active accounting config for instance {featureInstanceId}")
            return None, None, None

        connectorType = configRecord.get("connectorType")
        logger.info(f"Resolving connector '{connectorType}' for instance {featureInstanceId}")
        connector = self._registry.getConnector(connectorType)
        if not connector:
            logger.error(f"Accounting connector '{connectorType}' not found in registry")
            return None, None, configRecord

        plainConfig = self._decryptConfig(configRecord.get("encryptedConfig", ""))
        if not plainConfig:
            logger.error(f"Decrypted config is empty for connector '{connectorType}'")
        return connector, plainConfig, configRecord

    def _buildBookingFromPosition(self, position: Dict[str, Any]) -> AccountingBooking:
        """Build a standardised AccountingBooking from a TrusteePosition record."""
        lines = []
        debitAccount = position.get("debitAccountNumber")
        creditAccount = position.get("creditAccountNumber")
        amount = abs(position.get("bookingAmount", 0))

        if debitAccount:
            lines.append(AccountingBookingLine(
                accountNumber=debitAccount,
                debitAmount=amount,
                currency=position.get("bookingCurrency", "CHF"),
                taxCode=position.get("taxCode"),
                taxRate=position.get("vatPercentage"),
                description=position.get("desc", ""),
                costCenter=position.get("costCenter"),
                reference=position.get("bookingReference"),
            ))
        if creditAccount:
            lines.append(AccountingBookingLine(
                accountNumber=creditAccount,
                creditAmount=amount,
                currency=position.get("bookingCurrency", "CHF"),
                description=position.get("desc", ""),
                costCenter=position.get("costCenter"),
            ))

        return AccountingBooking(
            reference=position.get("bookingReference") or position.get("id", ""),
            bookingDate=position.get("valuta") or "",
            description=position.get("desc", ""),
            lines=lines,
        )

    async def pushPositionToAccounting(
        self,
        featureInstanceId: str,
        positionId: str,
        _resolvedConnector=None,
        _resolvedPlainConfig=None,
        _resolvedConfigRecord=None,
    ) -> SyncResult:
        """Push a single position to the configured accounting system.

        Optional _resolved* params allow pushBatchToAccounting to pass a pre-resolved
        connector/config so we don't decrypt per position (avoids rate-limit).
        """
        from modules.features.trustee.datamodelFeatureTrustee import TrusteePosition, TrusteeAccountingSync

        connector = _resolvedConnector
        plainConfig = _resolvedPlainConfig
        configRecord = _resolvedConfigRecord
        if not connector or not plainConfig:
            connector, plainConfig, configRecord = await self._resolveConnectorAndConfig(featureInstanceId)
        if not connector or not plainConfig:
            return SyncResult(success=False, errorMessage="No active accounting configuration found")

        connectorType = configRecord.get("connectorType", "")

        # Load position
        posRecords = self._trusteeInterface.db.getRecordset(TrusteePosition, recordFilter={"id": positionId})
        if not posRecords:
            return SyncResult(success=False, errorMessage=f"Position {positionId} not found")
        position = posRecords[0]

        # Build booking; skip if position has no accounts (not ready for sync)
        booking = self._buildBookingFromPosition(position)
        if not booking.lines:
            logger.info("Accounting sync skipped (no accounts): positionId=%s", positionId)
            return SyncResult(success=True, errorMessage="Position hat keine Kontierung (Soll-/Haben-Konto) – Sync übersprungen")

        # 1) First: ensure all documents are in RMA (upload or duplicate); collect Beleg-IDs for linking
        documentIds = []
        for key in ("documentId", "bankDocumentId"):
            docId = position.get(key)
            if docId:
                documentIds.append(docId)
        if documentIds:
            from modules.features.trustee.datamodelFeatureTrustee import TrusteeDocument as TrusteeDocumentModel
            logger.info("Accounting sync: positionId=%s, syncing %s document(s) to RMA ...", positionId, len(documentIds))
            belegIds = []
            belegLabels = []
            for documentId in documentIds:
                doc = self._trusteeInterface.getDocument(documentId)
                if not doc:
                    continue
                fileName = getattr(doc, "documentName", None) or "beleg.pdf"
                existingBelegId = getattr(doc, "externalBelegId", None)
                if existingBelegId:
                    logger.info("Accounting sync: document %s already has belegId=%s, skipping upload", documentId, existingBelegId)
                    belegIds.append(existingBelegId)
                    belegLabels.append(fileName)
                    continue
                docData = self._trusteeInterface.getDocumentData(documentId)
                if docData is None:
                    continue
                mimeType = getattr(doc, "documentMimeType", None) or "application/pdf"
                uploadResult = await connector.uploadDocument(
                    plainConfig,
                    fileName=fileName,
                    fileContent=docData,
                    mimeType=mimeType,
                    comment=booking.reference,
                )
                if not uploadResult.success:
                    errMsg = f"Dokument konnte nicht nach RMA hochgeladen werden: {uploadResult.errorMessage}"
                    logger.error(
                        "Accounting sync failed (document upload): positionId=%s, documentId=%s, error=%s",
                        positionId, documentId, uploadResult.errorMessage,
                    )
                    return SyncResult(success=False, errorMessage=errMsg)
                belegId = uploadResult.externalId
                if belegId:
                    self._trusteeInterface.db.recordModify(TrusteeDocumentModel, documentId, {"externalBelegId": belegId})
                    logger.info("Accounting sync: document uploaded & belegId=%s stored on document %s", belegId, documentId)
                else:
                    logger.info("Accounting sync: document uploaded but no belegId in response (409 duplicate?), fileName=%s", fileName)
                belegIds.append(belegId)
                belegLabels.append(fileName)
            if belegIds or belegLabels:
                booking.externalDocumentIds = belegIds
                booking.externalDocumentLabels = belegLabels
            logger.info("Accounting sync: positionId=%s, document sync done, pushing GL booking (POST /gl) ...", positionId)

        # Duplicate check: if locally marked as synced, verify with Buha system
        accountingSyncId = position.get("accountingSyncId")
        existingSyncs = self._trusteeInterface.db.getRecordset(
            TrusteeAccountingSync,
            recordFilter={"positionId": positionId, "connectorType": connectorType, "syncStatus": "synced"},
        )
        if accountingSyncId or existingSyncs:
            checkResult = await connector.isBookingSynced(plainConfig, booking)
            if checkResult.success:
                logger.info(
                    "Accounting sync skipped (verified in Buha): positionId=%s, reference=%s",
                    positionId, booking.reference,
                )
                return SyncResult(success=False, errorMessage="Position already synced to this system")
            # Not found in Buha (e.g. deleted there): clear local records and re-push
            logger.info(
                "Accounting sync: reference %s not found in Buha (deleted?), clearing local records and re-pushing positionId=%s",
                booking.reference, positionId,
            )
            if accountingSyncId:
                self._trusteeInterface.db.recordModify(TrusteePosition, positionId, {"accountingSyncId": None})
            for rec in existingSyncs:
                rid = rec.get("id")
                if rid:
                    self._trusteeInterface.db.recordDelete(TrusteeAccountingSync, rid)

        # 2) Then: push booking (with reference to document IDs so RMA can link)
        if not documentIds:
            logger.info("Accounting sync: positionId=%s, no documents, pushing GL booking (POST /gl) ...", positionId)
        result = await connector.pushBooking(plainConfig, booking)
        if not result.success:
            logger.error(
                "Accounting sync failed: positionId=%s, error=%s",
                positionId,
                result.errorMessage or "unknown",
            )

        # Save sync record
        import uuid
        syncRecord = {
            "id": str(uuid.uuid4()),
            "positionId": positionId,
            "featureInstanceId": featureInstanceId,
            "connectorType": connectorType,
            "externalId": result.externalId,
            "externalReference": result.externalReference,
            "syncStatus": "synced" if result.success else "error",
            "syncDirection": "push",
            "syncedAt": time.time() if result.success else None,
            "errorMessage": result.errorMessage,
            "bookingPayload": booking.model_dump(),
            "mandateId": self._trusteeInterface.mandateId,
        }
        self._trusteeInterface.db.recordCreate(TrusteeAccountingSync, syncRecord)

        # Write back external ID to position (source of truth for sync check)
        if result.success and result.externalId:
            self._trusteeInterface.db.recordModify(
                TrusteePosition, positionId, {"accountingSyncId": result.externalId}
            )

        # Update last sync on config record
        if configRecord:
            from modules.features.trustee.datamodelFeatureTrustee import TrusteeAccountingConfig
            updatePayload = {
                "lastSyncAt": time.time(),
                "lastSyncStatus": "success" if result.success else "error",
            }
            if result.success:
                updatePayload["lastSyncErrorMessage"] = None
            else:
                updatePayload["lastSyncErrorMessage"] = result.errorMessage or "Sync failed"
            self._trusteeInterface.db.recordModify(TrusteeAccountingConfig, configRecord["id"], updatePayload)

        return result

    async def pushBatchToAccounting(self, featureInstanceId: str, positionIds: List[str]) -> List[SyncResult]:
        """Push multiple positions sequentially. Resolves connector/config once to avoid decrypt rate-limit."""
        connector, plainConfig, configRecord = await self._resolveConnectorAndConfig(featureInstanceId)
        if not connector or not plainConfig:
            return [SyncResult(success=False, errorMessage="No active accounting configuration found") for _ in positionIds]
        results = []
        for positionId in positionIds:
            result = await self.pushPositionToAccounting(
                featureInstanceId, positionId,
                _resolvedConnector=connector, _resolvedPlainConfig=plainConfig, _resolvedConfigRecord=configRecord,
            )
            results.append(result)
        return results

    async def refreshChartOfAccounts(self, featureInstanceId: str) -> List[AccountingChart]:
        """Fetch the full chart of accounts from the external system and cache it locally on TrusteeAccountingConfig."""
        from modules.features.trustee.datamodelFeatureTrustee import TrusteeAccountingConfig

        connector, plainConfig, configRecord = await self._resolveConnectorAndConfig(featureInstanceId)
        if not connector or not plainConfig or not configRecord:
            logger.warning("refreshChartOfAccounts: no connector/config — nothing to cache")
            return []

        charts = await connector.getChartOfAccounts(plainConfig)
        serialised = json.dumps([{"accountNumber": c.accountNumber, "label": c.label, "accountType": c.accountType or ""} for c in charts], ensure_ascii=False)
        self._trusteeInterface.db.recordModify(TrusteeAccountingConfig, configRecord["id"], {
            "cachedChartOfAccounts": serialised,
            "chartCachedAt": time.time(),
        })
        logger.info(f"Cached {len(charts)} chart-of-accounts entries for instance {featureInstanceId}")
        return charts

    def _readCachedCharts(self, configRecord: Dict[str, Any]) -> List[AccountingChart]:
        """Deserialise the cached chart-of-accounts JSON from a config record. Returns [] on any error."""
        raw = configRecord.get("cachedChartOfAccounts")
        if not raw:
            return []
        try:
            items = json.loads(raw) if isinstance(raw, str) else raw
            return [AccountingChart(accountNumber=i["accountNumber"], label=i["label"], accountType=i.get("accountType", "")) for i in items]
        except Exception as e:
            logger.debug("Could not deserialise cached chart: %s", e)
            return []

    async def getChartOfAccounts(self, featureInstanceId: str, accountType: Optional[str] = None) -> List[AccountingChart]:
        """Return chart of accounts — from local cache if available, otherwise fetch externally and cache."""
        configRecord = await self.getActiveConfig(featureInstanceId)
        if not configRecord:
            return []

        charts = self._readCachedCharts(configRecord)
        if charts:
            logger.debug(f"Using cached chart of accounts ({len(charts)} entries) for instance {featureInstanceId}")
        else:
            logger.info(f"No cached chart — fetching live for instance {featureInstanceId}")
            charts = await self.refreshChartOfAccounts(featureInstanceId)

        if accountType:
            charts = [c for c in charts if c.accountType == accountType]
        return charts

    async def _getExpenseAccounts(self, featureInstanceId: str) -> List[AccountingChart]:
        """Load only expense accounts (Aufwandkonten) for use in AI prompts."""
        return await self.getChartOfAccounts(featureInstanceId, accountType="expense")

    async def testConnection(self, featureInstanceId: str) -> SyncResult:
        """Test the connection with the configured accounting system."""
        connector, plainConfig, _ = await self._resolveConnectorAndConfig(featureInstanceId)
        if not connector or not plainConfig:
            return SyncResult(success=False, errorMessage="No active accounting configuration found")
        return await connector.testConnection(plainConfig)
