# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""Abacus ERP accounting connector.

API docs: https://downloads.abacus.ch/fileadmin/ablage/abaconnect/htmlfiles/docs/restapi/abacus_rest_api.html
Auth: OAuth 2.0 Client Credentials (Service User).
Each Abacus instance has its own host URL; there is no central cloud endpoint.
Entity API uses OData V4 format.
"""

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

import aiohttp

from ..accountingConnectorBase import (
    BaseAccountingConnector,
    AccountingBooking,
    AccountingChart,
    ConnectorConfigField,
    SyncResult,
)

logger = logging.getLogger(__name__)


class AccountingConnectorAbacus(BaseAccountingConnector):

    def __init__(self):
        self._tokenCache: Dict[str, Dict[str, Any]] = {}

    def getConnectorType(self) -> str:
        return "abacus"

    def getConnectorLabel(self) -> Dict[str, str]:
        return {"en": "Abacus ERP", "de": "Abacus ERP", "fr": "Abacus ERP"}

    def getRequiredConfigFields(self) -> List[ConnectorConfigField]:
        return [
            ConnectorConfigField(
                key="apiBaseUrl",
                label={"en": "API Base URL", "de": "API Base URL", "fr": "URL de base API"},
                fieldType="text",
                secret=False,
                placeholder="e.g. https://abacus.meinefirma.ch/api/entity/v1/",
            ),
            ConnectorConfigField(
                key="clientName",
                label={"en": "Client Name", "de": "Mandantenname", "fr": "Nom du client"},
                fieldType="text",
                secret=False,
                placeholder="e.g. 7777",
            ),
            ConnectorConfigField(
                key="clientId",
                label={"en": "Client ID", "de": "Client-ID", "fr": "ID Client"},
                fieldType="text",
                secret=False,
            ),
            ConnectorConfigField(
                key="clientSecret",
                label={"en": "Client Secret", "de": "Client-Secret", "fr": "Secret Client"},
                fieldType="password",
                secret=True,
            ),
        ]

    def _buildBaseUrl(self, config: Dict[str, Any]) -> str:
        apiBaseUrl = str(config.get("apiBaseUrl") or "").strip()
        if not apiBaseUrl:
            raise ValueError("Missing required config: apiBaseUrl")
        if not apiBaseUrl.startswith("http"):
            apiBaseUrl = f"https://{apiBaseUrl}"
        return apiBaseUrl.rstrip("/")

    def _buildAuthBaseUrl(self, config: Dict[str, Any]) -> str:
        apiBaseUrl = str(config.get("apiBaseUrl") or "").strip()
        if not apiBaseUrl:
            raise ValueError("Missing required config: apiBaseUrl")
        if not apiBaseUrl.startswith("http"):
            apiBaseUrl = f"https://{apiBaseUrl}"
        apiBaseUrl = apiBaseUrl.rstrip("/")
        if "/api/entity/v1" in apiBaseUrl:
            return apiBaseUrl.split("/api/entity/v1", 1)[0]
        if "/api/" in apiBaseUrl:
            return apiBaseUrl.split("/api/", 1)[0]
        return apiBaseUrl

    async def _getAccessToken(self, config: Dict[str, Any]) -> Optional[str]:
        """Obtain an OAuth access token using client_credentials grant.

        Tokens are cached and refreshed when expired (default 600s).
        """
        cacheKey = f"{config.get('apiBaseUrl')}_{config.get('clientName')}_{config.get('clientId')}"
        cached = self._tokenCache.get(cacheKey)
        if cached and cached.get("expiresAt", 0) > time.time() + 30:
            return cached["accessToken"]

        baseUrl = self._buildAuthBaseUrl(config)

        try:
            async with aiohttp.ClientSession() as session:
                # Step 1: discover token endpoint
                async with session.get(f"{baseUrl}/.well-known/openid-configuration", timeout=aiohttp.ClientTimeout(total=10)) as resp:
                    if resp.status != 200:
                        logger.error(f"Abacus OIDC discovery failed: HTTP {resp.status}")
                        return None
                    oidc = await resp.json()

                tokenEndpoint = oidc.get("token_endpoint")
                if not tokenEndpoint:
                    logger.error("Abacus OIDC: no token_endpoint found")
                    return None

                # Step 2: request token
                credentials = base64.b64encode(f"{config['clientId']}:{config['clientSecret']}".encode()).decode()
                headers = {"Authorization": f"Basic {credentials}", "Content-Type": "application/x-www-form-urlencoded"}
                async with session.post(tokenEndpoint, headers=headers, data="grant_type=client_credentials", timeout=aiohttp.ClientTimeout(total=15)) as tokenResp:
                    if tokenResp.status != 200:
                        body = await tokenResp.text()
                        logger.error(f"Abacus token request failed: HTTP {tokenResp.status}: {body[:200]}")
                        return None
                    tokenData = await tokenResp.json()

                accessToken = tokenData.get("access_token")
                expiresIn = tokenData.get("expires_in", 600)
                self._tokenCache[cacheKey] = {"accessToken": accessToken, "expiresAt": time.time() + expiresIn}
                return accessToken

        except Exception as e:
            logger.error(f"Abacus token acquisition error: {e}")
            return None

    def _buildEntityUrl(self, config: Dict[str, Any], entity: str) -> str:
        baseUrl = self._buildBaseUrl(config)
        clientName = config.get("clientName")
        if not clientName:
            raise ValueError("Missing required config: clientName")
        return f"{baseUrl}/{clientName}/{entity}"

    async def _buildAuthHeaders(self, config: Dict[str, Any]) -> Optional[Dict[str, str]]:
        token = await self._getAccessToken(config)
        if not token:
            return None
        return {"Authorization": f"Bearer {token}", "Accept": "application/json", "Content-Type": "application/json"}

    async def testConnection(self, config: Dict[str, Any]) -> SyncResult:
        apiBaseUrl = str(config.get("apiBaseUrl") or "")
        clientName = str(config.get("clientName") or "")
        clientId = str(config.get("clientId") or "")
        clientSecret = str(config.get("clientSecret") or "")
        if not apiBaseUrl or not clientName or not clientId or not clientSecret:
            return SyncResult(
                success=False,
                errorMessage=(
                    f"Missing credentials: apiBaseUrl={bool(apiBaseUrl)}, "
                    f"clientName={bool(clientName)}, clientId={bool(clientId)}, "
                    f"clientSecret={bool(clientSecret)}"
                ),
            )
        headers = await self._buildAuthHeaders(config)
        if not headers:
            return SyncResult(success=False, errorMessage="Failed to obtain access token")
        try:
            async with aiohttp.ClientSession() as session:
                url = self._buildEntityUrl(config, "Accounts?$top=1")
                async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=15)) as resp:
                    if resp.status == 200:
                        return SyncResult(success=True)
                    body = await resp.text()
                    return SyncResult(success=False, errorMessage=f"HTTP {resp.status}: {body[:200]}")
        except Exception as e:
            return SyncResult(success=False, errorMessage=str(e))

    async def getChartOfAccounts(self, config: Dict[str, Any], accountType: Optional[str] = None) -> List[AccountingChart]:
        headers = await self._buildAuthHeaders(config)
        if not headers:
            return []

        charts: List[AccountingChart] = []
        url: Optional[str] = self._buildEntityUrl(config, "Accounts")

        try:
            async with aiohttp.ClientSession() as session:
                while url:
                    async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=30)) as resp:
                        if resp.status != 200:
                            break
                        data = await resp.json()

                    for item in data.get("value", []):
                        charts.append(AccountingChart(
                            accountNumber=str(item.get("AccountNumber", item.get("Id", ""))),
                            label=item.get("Name", item.get("Description", "")),
                            accountType=item.get("AccountType", None),
                        ))
                    url = data.get("@odata.nextLink")
        except Exception as e:
            logger.error(f"Abacus getChartOfAccounts error: {e}")
        return charts

    async def pushBooking(self, config: Dict[str, Any], booking: AccountingBooking) -> SyncResult:
        headers = await self._buildAuthHeaders(config)
        if not headers:
            return SyncResult(success=False, errorMessage="Failed to obtain access token")

        try:
            lines = []
            for line in booking.lines:
                entry: Dict[str, Any] = {
                    "AccountId": line.accountNumber,
                    "Text": line.description or booking.description,
                }
                if line.debitAmount > 0:
                    entry["DebitAmount"] = line.debitAmount
                if line.creditAmount > 0:
                    entry["CreditAmount"] = line.creditAmount
                if line.taxCode:
                    entry["TaxCode"] = line.taxCode
                if line.costCenter:
                    entry["CostCenterId"] = line.costCenter
                lines.append(entry)

            payload = {
                "JournalDate": booking.bookingDate,
                "Reference": booking.reference,
                "Text": booking.description,
                "Lines": lines,
            }

            async with aiohttp.ClientSession() as session:
                url = self._buildEntityUrl(config, "GeneralJournalEntries")
                async with session.post(url, headers=headers, json=payload, timeout=aiohttp.ClientTimeout(total=30)) as resp:
                    body = await resp.json() if resp.content_type and "json" in resp.content_type else {"raw": await resp.text()}
                    if resp.status in (200, 201):
                        externalId = str(body.get("Id", "")) if isinstance(body, dict) else None
                        return SyncResult(success=True, externalId=externalId, rawResponse=body)
                    return SyncResult(success=False, errorMessage=f"HTTP {resp.status}", rawResponse=body)
        except Exception as e:
            return SyncResult(success=False, errorMessage=str(e))

    async def getBookingStatus(self, config: Dict[str, Any], externalId: str) -> SyncResult:
        headers = await self._buildAuthHeaders(config)
        if not headers:
            return SyncResult(success=False, errorMessage="Failed to obtain access token")
        try:
            async with aiohttp.ClientSession() as session:
                url = self._buildEntityUrl(config, f"GeneralJournalEntries({externalId})")
                async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=15)) as resp:
                    if resp.status == 200:
                        return SyncResult(success=True, externalId=externalId)
                    return SyncResult(success=False, errorMessage=f"HTTP {resp.status}")
        except Exception as e:
            return SyncResult(success=False, errorMessage=str(e))

    async def getJournalEntries(self, config: Dict[str, Any], dateFrom: Optional[str] = None, dateTo: Optional[str] = None, accountNumbers: Optional[List[str]] = None) -> List[Dict[str, Any]]:
        """Read GeneralJournalEntries from Abacus (OData V4, paginated)."""
        headers = await self._buildAuthHeaders(config)
        if not headers:
            return []

        filterParts = []
        if dateFrom:
            filterParts.append(f"JournalDate ge {dateFrom}")
        if dateTo:
            filterParts.append(f"JournalDate le {dateTo}")
        queryParams = ""
        if filterParts:
            queryParams = "?$filter=" + " and ".join(filterParts)

        entries: List[Dict[str, Any]] = []
        url: Optional[str] = self._buildEntityUrl(config, f"GeneralJournalEntries{queryParams}")
        try:
            async with aiohttp.ClientSession() as session:
                while url:
                    async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=60)) as resp:
                        if resp.status != 200:
                            break
                        data = await resp.json()

                    for item in data.get("value", []):
                        lines = []
                        totalAmt = 0.0
                        for line in (item.get("Lines") or []):
                            debit = float(line.get("DebitAmount", 0))
                            credit = float(line.get("CreditAmount", 0))
                            lines.append({
                                "accountNumber": str(line.get("AccountId", "")),
                                "debitAmount": debit,
                                "creditAmount": credit,
                                "description": line.get("Text", ""),
                                "taxCode": line.get("TaxCode"),
                                "costCenter": line.get("CostCenterId"),
                            })
                            totalAmt += max(debit, credit)
                        entries.append({
                            "externalId": str(item.get("Id", "")),
                            "bookingDate": str(item.get("JournalDate", "")).split("T")[0],
                            "reference": item.get("Reference", ""),
                            "description": item.get("Text", ""),
                            "currency": "CHF",
                            "totalAmount": totalAmt,
                            "lines": lines,
                        })
                    url = data.get("@odata.nextLink")
        except Exception as e:
            logger.error(f"Abacus getJournalEntries error: {e}")
        return entries

    async def getCustomers(self, config: Dict[str, Any]) -> List[Dict[str, Any]]:
        headers = await self._buildAuthHeaders(config)
        if not headers:
            return []
        try:
            async with aiohttp.ClientSession() as session:
                url = self._buildEntityUrl(config, "Debtors")
                async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=30)) as resp:
                    if resp.status != 200:
                        return []
                    data = await resp.json()
                    return data.get("value", [])
        except Exception as e:
            logger.error(f"Abacus getCustomers error: {e}")
            return []

    async def getVendors(self, config: Dict[str, Any]) -> List[Dict[str, Any]]:
        headers = await self._buildAuthHeaders(config)
        if not headers:
            return []
        try:
            async with aiohttp.ClientSession() as session:
                url = self._buildEntityUrl(config, "Creditors")
                async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=30)) as resp:
                    if resp.status != 200:
                        return []
                    data = await resp.json()
                    return data.get("value", [])
        except Exception as e:
            logger.error(f"Abacus getVendors error: {e}")
            return []
