"""
vTiger Cloud CRM Integration Service
Handles subscription and sales order data retrieval
"""
import logging
import json
import re
import html as html_lib
import aiohttp
from decimal import Decimal
from typing import List, Dict, Optional, Any
from app.core.config import settings

logger = logging.getLogger(__name__)


class VTigerService:
    """Service for integrating with vTiger Cloud CRM via REST API"""
    
    def __init__(self):
        self.base_url = getattr(settings, 'VTIGER_URL', None)
        self.username = getattr(settings, 'VTIGER_USERNAME', None)
        self.api_key = getattr(settings, 'VTIGER_API_KEY', None)
        
        # REST API endpoint
        if self.base_url:
            self.rest_endpoint = f"{self.base_url}/restapi/v1/vtiger/default"
        else:
            self.rest_endpoint = None
        
        if not all([self.base_url, self.username, self.api_key]):
            logger.warning("⚠️ vTiger credentials not fully configured")

        self.last_query_status: Optional[int] = None
        self.last_query_error: Optional[Dict] = None
    
    def _get_auth(self):
        """Get HTTP Basic Auth credentials"""
        if not self.api_key:
            raise ValueError("VTIGER_API_KEY not configured")
        return aiohttp.BasicAuth(self.username, self.api_key)

    @staticmethod
    def _sanitize_vtiger_id(raw_value: Optional[str]) -> Optional[str]:
        """Allow-list known vTiger id characters before embedding in query strings."""
        if not raw_value:
            return None
        candidate = str(raw_value).strip()
        if re.match(r"^[A-Za-z0-9_\-x]+$", candidate):
            return candidate
        return None

    @staticmethod
    def _extract_timelog_hours(timelog_data: Dict[str, Any]) -> Decimal:
        """Extract hours from common vTiger timelog fields and normalize to decimal hours."""
        raw_value = None
        field_used = None

        for key in ("time_spent", "duration", "total_hours", "hours"):
            value = timelog_data.get(key)
            if value not in (None, ""):
                raw_value = value
                field_used = key
                break

        if raw_value in (None, ""):
            return Decimal("0")

        if field_used == "duration":
            try:
                seconds = Decimal(str(raw_value))
                return (seconds / Decimal(3600)).quantize(Decimal("0.01"))
            except Exception:
                pass

        raw_str = str(raw_value).strip().lower()

        time_match = re.match(r"^(\d+):(\d+)(?::(\d+))?$", raw_str)
        if time_match:
            hours = Decimal(int(time_match.group(1)))
            minutes = Decimal(int(time_match.group(2))) / Decimal(60)
            seconds = Decimal(int(time_match.group(3) or 0)) / Decimal(3600)
            return (hours + minutes + seconds).quantize(Decimal("0.01"))

        if "h" in raw_str or "m" in raw_str or "s" in raw_str:
            total_hours = Decimal("0")
            hours_match = re.search(r"(\d+(?:[\.,]\d+)?)\s*h", raw_str)
            mins_match = re.search(r"(\d+(?:[\.,]\d+)?)\s*m", raw_str)
            secs_match = re.search(r"(\d+(?:[\.,]\d+)?)\s*s", raw_str)

            if hours_match:
                total_hours += Decimal(hours_match.group(1).replace(",", "."))
            if mins_match:
                total_hours += Decimal(mins_match.group(1).replace(",", ".")) / Decimal(60)
            if secs_match:
                total_hours += Decimal(secs_match.group(1).replace(",", ".")) / Decimal(3600)

            return total_hours.quantize(Decimal("0.01"))

        try:
            return Decimal(raw_str.replace(",", ".")).quantize(Decimal("0.01"))
        except Exception:
            return Decimal("0")
    
    async def query(self, query_string: str) -> List[Dict]:
        """
        Execute a query on vTiger REST API
        
        Args:
            query_string: SQL-like query (e.g., "SELECT * FROM Accounts;")
            
        Returns:
            List of records
        """
        if not self.rest_endpoint:
            raise ValueError("VTIGER_URL not configured")
        
        try:
            auth = self._get_auth()
            self.last_query_status = None
            self.last_query_error = None
            async with aiohttp.ClientSession() as session:
                async with session.get(
                    f"{self.rest_endpoint}/query",
                    params={"query": query_string},
                    auth=auth
                ) as response:
                    text = await response.text()
                    self.last_query_status = response.status
                    
                    if response.status == 200:
                        # vTiger returns text/json instead of application/json
                        import json
                        try:
                            data = json.loads(text)
                        except json.JSONDecodeError as e:
                            logger.error(f"❌ Invalid JSON in query response: {text[:200]}")
                            return []
                        
                        if data.get('success'):
                            result = data.get('result', [])
                            logger.info(f"✅ Query returned {len(result)} records")
                            self.last_query_error = None
                            return result
                        else:
                            self.last_query_error = data.get('error')
                            logger.error(f"❌ vTiger query failed: {self.last_query_error}")
                            return []
                    else:
                        try:
                            parsed = json.loads(text) if text else {}
                            self.last_query_error = parsed.get('error')
                        except Exception:
                            self.last_query_error = None
                        if response.status == 429:
                            logger.warning(f"⚠️ vTiger query rate-limited (HTTP {response.status})")
                        else:
                            logger.error(f"❌ vTiger query HTTP error {response.status}")
                        logger.error(f"Query: {query_string}")
                        logger.error(f"Response: {text[:500]}")
                        return []
        except Exception as e:
            self.last_query_status = None
            self.last_query_error = {"message": str(e)}
            logger.error(f"❌ vTiger query error: {e}")
            return []
    
    async def get_account_by_id(self, account_id: str) -> Optional[Dict]:
        """
        Fetch a single account by ID from vTiger
        
        Args:
            account_id: vTiger account ID (e.g., "3x760")
            
        Returns:
            Account record or None
        """
        if not account_id:
            logger.warning("⚠️ No account ID provided")
            return None
        
        try:
            query = f"SELECT * FROM Accounts WHERE id='{account_id}' LIMIT 1;"
            results = await self.query(query)
            
            if results and len(results) > 0:
                logger.info(f"✅ Found account {account_id}")
                return results[0]
            else:
                logger.warning(f"⚠️ No account found with ID {account_id}")
                return None
                
        except Exception as e:
            logger.error(f"❌ Error fetching account {account_id}: {e}")
            return None
    
    async def update_account(self, account_id: str, update_data: Dict) -> bool:
        """
        Update an account in vTiger
        
        Args:
            account_id: vTiger account ID (e.g., "3x760")
            update_data: Dictionary of fields to update
            
        Returns:
            True if successful, False otherwise
        """
        if not self.rest_endpoint:
            raise ValueError("VTIGER_URL not configured")
        
        try:
            # Fetch current account first - vTiger requires modifiedtime for updates
            current_account = await self.get_account_by_id(account_id)
            if not current_account:
                logger.error(f"❌ Account {account_id} not found for update")
                return False
            
            auth = self._get_auth()
            
            # Build payload with current data + updates
            # Include essential fields for vTiger update validation
            payload = {
                'id': account_id,
                'accountname': current_account.get('accountname'),
                'assigned_user_id': current_account.get('assigned_user_id'),
                'modifiedtime': current_account.get('modifiedtime'),
                **update_data
            }
            
            logger.info(f"🔄 vTiger update payload: {payload}")
            
            async with aiohttp.ClientSession() as session:
                async with session.post(
                    f"{self.rest_endpoint}/update",
                    json=payload,
                    auth=auth
                ) as response:
                    text = await response.text()
                    
                    if response.status == 200:
                        import json
                        try:
                            data = json.loads(text)
                            if data.get('success'):
                                logger.info(f"✅ Updated vTiger account {account_id}")
                                return True
                            else:
                                logger.error(f"❌ vTiger update failed: {data.get('error')}")
                                logger.error(f"Full response: {text}")
                                return False
                        except json.JSONDecodeError:
                            logger.error(f"❌ Invalid JSON in update response: {text[:200]}")
                            return False
                    else:
                        logger.error(f"❌ vTiger update HTTP error {response.status}")
                        logger.error(f"Full response: {text[:2000]}")
                        return False
                        
        except Exception as e:
            logger.error(f"❌ Error updating vTiger account {account_id}: {e}")
            return False
    
    async def get_customer_sales_orders(self, vtiger_account_id: str) -> List[Dict]:
        """
        Fetch sales orders for a customer from vTiger
        
        Args:
            vtiger_account_id: vTiger account ID (e.g., "3x760")
            
        Returns:
            List of sales order records
        """
        if not vtiger_account_id:
            logger.warning("⚠️ No vTiger account ID provided")
            return []
        
        try:
            # Query for sales orders linked to this account
            query = f"SELECT * FROM SalesOrder WHERE account_id='{vtiger_account_id}';"
            
            logger.info(f"🔍 Fetching sales orders for vTiger account {vtiger_account_id}")
            orders = await self.query(query)
            
            logger.info(f"✅ Found {len(orders)} sales orders")
            return orders
            
        except Exception as e:
            logger.error(f"❌ Error fetching sales orders: {e}")
            return []
    
    async def get_customer_subscriptions(self, vtiger_account_id: str) -> List[Dict]:
        """
        Fetch subscriptions for a customer from vTiger
        
        Args:
            vtiger_account_id: vTiger account ID (e.g., "3x760")
            
        Returns:
            List of subscription records
        """
        if not vtiger_account_id:
            logger.warning("⚠️ No vTiger account ID provided")
            return []
        
        try:
            # Query for subscriptions linked to this account (note: module name is singular "Subscription")
            query = f"SELECT * FROM Subscription WHERE account_id='{vtiger_account_id}';"
            
            logger.info(f"🔍 Fetching subscriptions for vTiger account {vtiger_account_id}")
            subscriptions = await self.query(query)
            
            logger.info(f"✅ Found {len(subscriptions)} subscriptions")
            return subscriptions
            
        except Exception as e:
            logger.error(f"❌ Error fetching subscriptions: {e}")
            return []
    
    async def get_subscription(self, subscription_id: str) -> Optional[Dict]:
        """
        Fetch full subscription details with LineItems from vTiger
        
        Args:
            subscription_id: vTiger subscription ID (e.g., "72x123")
            
        Returns:
            Subscription record with LineItems array or None
        """
        if not self.rest_endpoint:
            raise ValueError("VTIGER_URL not configured")
        
        try:
            auth = self._get_auth()
            
            async with aiohttp.ClientSession() as session:
                url = f"{self.rest_endpoint}/retrieve?id={subscription_id}"
                
                async with session.get(url, auth=auth) as response:
                    if response.status == 200:
                        text = await response.text()
                        data = json.loads(text)
                        if data.get('success'):
                            logger.info(f"✅ Retrieved subscription {subscription_id}")
                            return data.get('result')
                        else:
                            logger.error(f"❌ vTiger API error: {data.get('error')}")
                            return None
                    else:
                        logger.error(f"❌ HTTP {response.status} retrieving subscription")
                        return None
                        
        except Exception as e:
            logger.error(f"❌ Error retrieving subscription: {e}")
            return None
    
    async def get_account(self, vtiger_account_id: str) -> Optional[Dict]:
        """
        Fetch account details from vTiger
        
        Args:
            vtiger_account_id: vTiger account ID (e.g., "3x760")
            
        Returns:
            Account record with BMC Låst field (cf_accounts_bmclst) or None
        """
        if not vtiger_account_id:
            logger.warning("⚠️ No vTiger account ID provided")
            return None
        
        try:
            # Query for account by ID
            query = f"SELECT * FROM Accounts WHERE id='{vtiger_account_id}';"
            
            logger.info(f"🔍 Fetching account details for {vtiger_account_id}")
            accounts = await self.query(query)
            
            if accounts:
                logger.info(f"✅ Found account: {accounts[0].get('accountname', 'Unknown')}")
                return accounts[0]
            else:
                logger.warning(f"⚠️ No account found with ID {vtiger_account_id}")
                return None
            
        except Exception as e:
            logger.error(f"❌ Error fetching account: {e}")
            return None
    
    async def test_connection(self) -> bool:
        """
        Test vTiger connection using /me endpoint
        
        Returns:
            True if connection successful
        """
        if not self.rest_endpoint:
            raise ValueError("VTIGER_URL not configured in .env")
        
        try:
            auth = self._get_auth()
            logger.info(f"🔑 Testing vTiger connection...")
            
            async with aiohttp.ClientSession() as session:
                async with session.get(
                    f"{self.rest_endpoint}/me",
                    auth=auth
                ) as response:
                    if response.status == 200:
                        # vTiger returns text/json instead of application/json
                        text = await response.text()
                        import json
                        data = json.loads(text)
                        
                        if data.get('success'):
                            user_name = data['result'].get('user_name')
                            logger.info(f"✅ vTiger connection successful (user: {user_name})")
                            return True
                        else:
                            logger.error(f"❌ vTiger API returned success=false: {data}")
                            return False
                    else:
                        error_text = await response.text()
                        logger.error(f"❌ vTiger connection failed: HTTP {response.status}: {error_text}")
                        return False
                        
        except Exception as e:
            logger.error(f"❌ vTiger connection error: {e}")
            return False
    
    async def create_subscription(
        self,
        account_id: str,
        subject: str,
        startdate: str,
        generateinvoiceevery: str,
        subscriptionstatus: str = "Active",
        enddate: Optional[str] = None,
        products: Optional[List[Dict]] = None
    ) -> Dict:
        """
        Create a new subscription in vTiger
        
        Args:
            account_id: vTiger account ID (e.g., "3x123")
            subject: Subscription subject/name
            startdate: Start date (YYYY-MM-DD)
            generateinvoiceevery: Frequency ("Monthly", "Quarterly", "Yearly")
            subscriptionstatus: Status ("Active", "Cancelled", "Stopped")
            enddate: End date (YYYY-MM-DD), optional
            products: List of products [{"productid": "id", "quantity": 1, "listprice": 100}]
        """
        if not self.rest_endpoint:
            raise ValueError("VTIGER_URL not configured")
        
        try:
            auth = self._get_auth()
            
            # Build subscription data
            subscription_data = {
                "account_id": account_id,
                "subject": subject,
                "startdate": startdate,
                "generateinvoiceevery": generateinvoiceevery,
                "subscriptionstatus": subscriptionstatus,
            }
            
            if enddate:
                subscription_data["enddate"] = enddate
            
            # Add products if provided
            if products:
                subscription_data["products"] = products
            
            async with aiohttp.ClientSession() as session:
                create_url = f"{self.rest_endpoint}/create"
                
                payload = {
                    "elementType": "Subscription",
                    "element": subscription_data
                }
                
                logger.info(f"📤 Creating subscription: {subject}")
                
                async with session.post(create_url, json=payload, auth=auth) as response:
                    if response.status == 200:
                        data = await response.json()
                        if data.get("success"):
                            result = data.get("result", {})
                            logger.info(f"✅ Created subscription: {result.get('id')}")
                            return result
                        else:
                            error_msg = data.get("error", {}).get("message", "Unknown error")
                            raise Exception(f"vTiger API error: {error_msg}")
                    else:
                        error_text = await response.text()
                        raise Exception(f"HTTP {response.status}: {error_text}")
                        
        except Exception as e:
            logger.error(f"❌ Error creating subscription: {e}")
            raise
    
    async def update_subscription(self, subscription_id: str, updates: Dict, line_items: List[Dict] = None) -> Dict:
        """
        Update a subscription in vTiger with optional line item price updates
        
        Args:
            subscription_id: vTiger subscription ID (e.g., "72x123")
            updates: Dictionary of fields to update (subject, startdate, etc.)
            line_items: Optional list of line items with updated prices
                       Each item: {"productid": "6x123", "quantity": "3", "listprice": "299.00"}
        """
        if not self.rest_endpoint:
            raise ValueError("VTIGER_URL not configured")
        
        try:
            auth = self._get_auth()
            
            async with aiohttp.ClientSession() as session:
                # For custom fields (cf_*), we need to retrieve first for context
                if any(k.startswith('cf_') for k in updates.keys()):
                    logger.info(f"📥 Retrieving subscription {subscription_id} for custom field update")
                    current_sub = await self.get_subscription(subscription_id)
                    if current_sub:
                        # Only include essential fields + custom field update
                        essential_fields = ['id', 'account_id', 'subject', 'startdate', 
                                          'generateinvoiceevery', 'subscriptionstatus']
                        element_data = {
                            k: current_sub[k] 
                            for k in essential_fields 
                            if k in current_sub
                        }
                        element_data.update(updates)
                        element_data['id'] = subscription_id
                    else:
                        element_data = {"id": subscription_id, **updates}
                else:
                    element_data = {"id": subscription_id, **updates}
                
                update_url = f"{self.rest_endpoint}/update"
                
                # Add LineItems if provided
                if line_items:
                    element_data["LineItems"] = line_items
                    logger.info(f"📝 Updating {len(line_items)} line items")
                
                payload = {
                    "elementType": "Subscription",
                    "element": element_data
                }
                
                logger.info(f"📤 Updating subscription {subscription_id}: {list(updates.keys())}")
                logger.debug(f"Payload: {json.dumps(payload, indent=2)}")
                
                async with session.post(update_url, json=payload, auth=auth) as response:
                    if response.status == 200:
                        text = await response.text()
                        data = json.loads(text)
                        if data.get("success"):
                            result = data.get("result", {})
                            logger.info(f"✅ Updated subscription: {subscription_id}")
                            return result
                        else:
                            error_msg = data.get("error", {}).get("message", "Unknown error")
                            logger.error(f"❌ vTiger error response: {data.get('error')}")
                            raise Exception(f"vTiger API error: {error_msg}")
                    else:
                        error_text = await response.text()
                        logger.error(f"❌ vTiger HTTP error: {error_text[:500]}")
                        raise Exception(f"HTTP {response.status}: {error_text}")
                        
        except Exception as e:
            logger.error(f"❌ Error updating subscription: {e}")
            raise
    
    async def get_service_contracts(self, account_id: Optional[str] = None, active_only: bool = True) -> List[Dict]:
        """
        Fetch service contracts from vTiger
        
        Args:
            account_id: Optional - filter by account ID (e.g., "3x760")
            
        Returns:
            List of service contract records with account_id included
        """
        try:
            query_filters = []
            safe_account_id = self._sanitize_vtiger_id(account_id)

            if account_id and not safe_account_id:
                logger.warning("⚠️ Rejected unsafe account_id for service contract query")
                return []

            if safe_account_id:
                query_filters.append(f"sc_related_to='{safe_account_id}'")

            if active_only:
                query_filters.append("contract_status='Active'")

            where_clause = f" WHERE {' AND '.join(query_filters)}" if query_filters else ""
            query = f"SELECT * FROM ServiceContracts{where_clause};"

            if safe_account_id:
                logger.info(f"🔍 Fetching service contracts for account {safe_account_id} (active_only={active_only})")
            else:
                logger.info(f"🔍 Fetching all service contracts (active_only={active_only})")
            
            contracts = await self.query(query)
            logger.info(f"✅ Found {len(contracts)} service contracts")
            
            # Normalize fields used by the wizard
            for contract in contracts:
                if 'account_id' not in contract:
                    contract['account_id'] = (
                        contract.get('accountid')
                        or contract.get('cf_service_contracts_account')
                        or contract.get('sc_related_to')
                        or ""
                    )

                if 'contract_number' not in contract:
                    contract['contract_number'] = contract.get('contract_no', '')

                if not contract['account_id']:
                    logger.warning(
                        f"⚠️ Contract {contract.get('id')} has no account_id, filling null"
                    )
            
            return contracts
            
        except Exception as e:
            logger.error(f"❌ Error fetching service contracts: {e}")
            return []
    
    async def get_service_contract_cases(self, contract_id: str) -> List[Dict]:
        """
        Fetch cases linked to a service contract
        
        Args:
            contract_id: vTiger service contract ID (e.g., "75x123")
            
        Returns:
            List of case records linked to contract
        """
        try:
            # Query cases linked via service contract reference
            query = (
                "SELECT * FROM Cases WHERE "
                f"servicecontract_id='{contract_id}' OR "
                f"parent_id='{contract_id}';"
            )
            logger.info(f"🔍 Fetching cases for service contract {contract_id}")
            
            cases = await self.query(query)
            logger.info(f"✅ Found {len(cases)} cases for contract")
            return cases
            
        except Exception as e:
            logger.error(f"❌ Error fetching contract cases: {e}")
            return []
    
    async def get_service_contract_timelogs(self, contract_id: str) -> List[Dict]:
        """
        Fetch time logs linked to a service contract
        
        Args:
            contract_id: vTiger service contract ID (e.g., "75x123")
            
        Returns:
            List of timelog records linked to contract
        """
        try:
            # Query timelogs linked via service contract reference
            query = (
                "SELECT * FROM Timelog WHERE "
                f"relatedto='{contract_id}';"
            )
            logger.info(f"🔍 Fetching timelogs for service contract {contract_id}")

            timelogs = await self.query(query)

            # Fallback: timelogs may be linked to cases instead of the contract
            if not timelogs:
                cases = await self.get_service_contract_cases(contract_id)
                case_ids = [case.get('id') for case in cases if case.get('id')]

                if case_ids:
                    logger.info(
                        f"ℹ️ No direct timelogs found; querying by {len(case_ids)} case IDs"
                    )
                    timelog_map = {}
                    chunk_size = 20

                    for index in range(0, len(case_ids), chunk_size):
                        chunk = case_ids[index:index + chunk_size]
                        ids = "', '".join(chunk)
                        chunk_query = (
                            "SELECT * FROM Timelog WHERE "
                            f"relatedto IN ('{ids}');"
                        )
                        chunk_rows = await self.query(chunk_query)
                        for row in chunk_rows:
                            if row.get('id'):
                                timelog_map[row['id']] = row

                    timelogs = list(timelog_map.values())

            logger.info(f"✅ Found {len(timelogs)} timelogs for contract")
            return timelogs
            
        except Exception as e:
            logger.error(f"❌ Error fetching contract timelogs: {e}")
            return []

    async def get_service_contract_customers(self) -> List[Dict[str, str]]:
        """Fetch account list that has at least one service contract."""
        contracts = await self.get_service_contracts(active_only=False)
        account_ids = sorted({
            c.get("account_id")
            for c in contracts
            if c.get("account_id")
        })

        if not account_ids:
            return []

        customers: Dict[str, Dict[str, str]] = {}
        chunk_size = 20
        for i in range(0, len(account_ids), chunk_size):
            chunk = account_ids[i:i + chunk_size]
            ids = "', '".join(chunk)
            query = (
                "SELECT id, accountname FROM Accounts "
                f"WHERE id IN ('{ids}');"
            )
            rows = await self.query(query)
            for row in rows:
                account_id = row.get("id")
                if account_id:
                    customers[account_id] = {
                        "account_id": account_id,
                        "account_name": row.get("accountname") or account_id,
                    }

        # Fallback names for accounts not returned by Accounts query
        for account_id in account_ids:
            if account_id not in customers:
                customers[account_id] = {
                    "account_id": account_id,
                    "account_name": account_id,
                }

        return sorted(customers.values(), key=lambda x: x.get("account_name", "").lower())

    @staticmethod
    def _is_closed_case(status: Optional[str]) -> bool:
        if not status:
            return False
        normalized = status.strip().lower()
        return normalized in {"closed", "resolved", "done", "completed", "lukket"}

    @staticmethod
    def _first_non_empty(record: Dict[str, Any], keys: tuple[str, ...]) -> str:
        for key in keys:
            value = record.get(key)
            if value not in (None, ""):
                text = str(value).strip()
                if text:
                    return text
        return ""

    @staticmethod
    def _html_to_text(value: Optional[str]) -> str:
        """Convert HTML snippets from CRM fields into readable plain text."""
        if value in (None, ""):
            return ""

        text = str(value)
        text = re.sub(r"(?i)<br\s*/?>", "\n", text)
        text = re.sub(r"(?is)<(script|style).*?>.*?</\1>", " ", text)
        text = re.sub(r"(?is)<[^>]+>", " ", text)
        text = html_lib.unescape(text)
        text = text.replace("\xa0", " ")
        text = re.sub(r"[ \t]+", " ", text)
        text = re.sub(r"\n{3,}", "\n\n", text)
        return text.strip()

    @staticmethod
    def _extract_vtiger_record_id(raw_id: Optional[str]) -> Optional[str]:
        if not raw_id:
            return None
        text = str(raw_id).strip()
        if not text:
            return None
        if re.match(r"^\d+x\d+$", text):
            return text.split("x", 1)[1]
        if text.isdigit():
            return text
        return None

    def _build_vtiger_record_url(self, module: str, raw_id: Optional[str]) -> str:
        if not self.base_url:
            return ""
        record_id = self._extract_vtiger_record_id(raw_id)
        if not record_id:
            return ""
        base = str(self.base_url).rstrip("/")
        if module == "Cases":
            return f"{base}/view/detail?module=Cases&id={record_id}&viewtype=summary"
        if module == "Timelog":
            return f"{base}/view/detail?module=Timelog&id={record_id}&viewtype=summary"
        return f"{base}/index.php?module={module}&view=Detail&record={record_id}"

    @staticmethod
    def _to_initials(value: Optional[str]) -> str:
        if not value:
            return "--"
        text = str(value).strip()
        if not text:
            return "--"

        # Raw vTiger entity IDs are not human initials.
        if re.match(r"^\d+x\d+$", text):
            return "--"

        tokens = [token for token in re.split(r"[^A-Za-z0-9]+", text) if token]
        if not tokens:
            return "--"

        if len(tokens) == 1:
            return tokens[0][:2].upper()

        return f"{tokens[0][0]}{tokens[1][0]}".upper()

    async def _resolve_user_initials(self, user_refs: List[str]) -> Dict[str, str]:
        initials_by_ref: Dict[str, str] = {}
        safe_ids = []
        for raw_ref in user_refs:
            safe_ref = self._sanitize_vtiger_id(raw_ref)
            if safe_ref:
                safe_ids.append(safe_ref)

        if not safe_ids:
            return initials_by_ref

        unique_ids = sorted(set(safe_ids))
        chunk_size = 20
        for index in range(0, len(unique_ids), chunk_size):
            chunk = unique_ids[index:index + chunk_size]
            id_list = "', '".join(chunk)
            rows = await self.query(
                "SELECT id, first_name, last_name, user_name FROM Users "
                f"WHERE id IN ('{id_list}');"
            )
            for row in rows:
                user_id = row.get("id")
                if not user_id:
                    continue
                full_name = " ".join(
                    part for part in [row.get("first_name"), row.get("last_name")] if part
                ).strip()
                source = full_name or row.get("user_name") or user_id
                initials_by_ref[user_id] = self._to_initials(source)

            # Some timelog owners are groups, not user records.
            group_rows = await self.query(
                "SELECT id, groupname FROM Groups "
                f"WHERE id IN ('{id_list}');"
            )
            for row in group_rows:
                group_id = row.get("id")
                if not group_id or group_id in initials_by_ref:
                    continue
                initials_by_ref[group_id] = self._to_initials(row.get("groupname") or group_id)

        return initials_by_ref

    async def _resolve_contact_names(self, contact_refs: List[str]) -> Dict[str, str]:
        names_by_ref: Dict[str, str] = {}
        safe_ids = []

        for raw_ref in contact_refs:
            if not raw_ref:
                continue
            text = str(raw_ref).strip()
            safe_ref = self._sanitize_vtiger_id(text)
            if safe_ref and re.match(r"^\d+x\d+$", safe_ref):
                safe_ids.append(safe_ref)
            elif text:
                names_by_ref[text] = text

        if not safe_ids:
            return names_by_ref

        unique_ids = sorted(set(safe_ids))
        chunk_size = 20
        for index in range(0, len(unique_ids), chunk_size):
            chunk = unique_ids[index:index + chunk_size]
            id_list = "', '".join(chunk)
            rows = await self.query(
                "SELECT id, firstname, lastname, salutationtype FROM Contacts "
                f"WHERE id IN ('{id_list}');"
            )
            for row in rows:
                contact_id = row.get("id")
                if not contact_id:
                    continue
                parts = [
                    str(row.get("salutationtype") or "").strip(),
                    str(row.get("firstname") or "").strip(),
                    str(row.get("lastname") or "").strip(),
                ]
                full_name = " ".join(part for part in parts if part)
                names_by_ref[contact_id] = full_name or contact_id

        return names_by_ref

    async def get_service_contract_report_data(self, account_id: str, contract_id: str) -> Dict[str, Any]:
        """Build report payload for selected customer + service contract."""
        safe_account_id = self._sanitize_vtiger_id(account_id)
        safe_contract_id = self._sanitize_vtiger_id(contract_id)
        if not safe_account_id or not safe_contract_id:
            raise ValueError("Invalid account_id or contract_id")

        contract_rows = await self.query(
            f"SELECT * FROM ServiceContracts WHERE id='{safe_contract_id}' LIMIT 1;"
        )
        if not contract_rows:
            raise ValueError("Service contract was not found")

        contract = contract_rows[0]
        contract_account_id = (
            contract.get("account_id")
            or contract.get("accountid")
            or contract.get("cf_service_contracts_account")
            or contract.get("sc_related_to")
            or ""
        )

        # If contract is linked to a different customer, return clear validation error.
        if contract_account_id and contract_account_id != safe_account_id:
            raise ValueError("Service contract does not belong to selected customer")

        account = await self.get_account_by_id(safe_account_id)
        account_name = (account or {}).get("accountname") or safe_account_id

        cases = await self.get_service_contract_cases(safe_contract_id)
        timelogs = await self.get_service_contract_timelogs(safe_contract_id)

        contact_ref_candidates = [
            str(
                case.get("contact_id")
                or case.get("contactid")
                or case.get("parent_contact_id")
                or case.get("contactname")
                or case.get("cf_contact_person")
                or ""
            )
            for case in cases
            if (
                case.get("contact_id")
                or case.get("contactid")
                or case.get("parent_contact_id")
                or case.get("contactname")
                or case.get("cf_contact_person")
            )
        ]
        contact_name_map = await self._resolve_contact_names(contact_ref_candidates)

        user_ref_candidates = [
            str(
                log.get("assigned_user_id")
                or log.get("modifiedby")
                or log.get("created_user_id")
                or ""
            )
            for log in timelogs
            if (
                log.get("assigned_user_id")
                or log.get("modifiedby")
                or log.get("created_user_id")
            )
        ]
        user_initials_map = await self._resolve_user_initials(user_ref_candidates)

        case_map: Dict[str, Dict[str, Any]] = {}
        report_cases: List[Dict[str, Any]] = []

        for case in cases:
            case_id = case.get("id")
            if not case_id:
                continue

            case_payload = {
                "id": case_id,
                "cc_number": self._first_non_empty(
                    case,
                    ("ticket_no", "case_no", "caseno", "ticketid", "cf_case_number")
                ) or case_id,
                "title": self._first_non_empty(
                    case,
                    ("ticket_title", "tickettitle", "subject", "title")
                ) or f"Case {case_id}",
                "description": self._html_to_text(
                    self._first_non_empty(
                        case,
                        ("description", "ticketdescription", "solution", "comments", "comment")
                    )
                ) or "Ingen beskrivelse",
                "contact_person": contact_name_map.get(
                    str(
                        case.get("contact_id")
                        or case.get("contactid")
                        or case.get("parent_contact_id")
                        or case.get("contactname")
                        or case.get("cf_contact_person")
                        or ""
                    ),
                    self._first_non_empty(
                        case,
                        ("contactname", "contact_id", "contactid", "cf_contact_person")
                    ) or "-"
                ),
                "vtiger_url": self._build_vtiger_record_url("Cases", case_id),
                "status": case.get("ticketstatus") or case.get("status"),
                "priority": case.get("ticketpriorities") or case.get("priority"),
                "total_hours": Decimal("0"),
                "timelog_count": 0,
                "timelogs": [],
            }
            case_map[case_id] = case_payload
            report_cases.append(case_payload)

        unmatched_case_id = "UNMAPPED"
        for log in timelogs:
            related_case_id = (
                log.get("relatedto")
                or log.get("case_id")
                or log.get("ticket_id")
                or log.get("parent_id")
            )
            raw_user = (
                log.get("assigned_user_id")
                or log.get("modifiedby")
                or log.get("created_user_id")
                or ""
            )
            hours = self._extract_timelog_hours(log)
            timelog_payload = {
                "id": log.get("id") or "",
                "related_case_id": related_case_id,
                "worked_date": log.get("date_start") or log.get("createdtime") or log.get("modifiedtime"),
                "user_name": raw_user,
                "employee_initials": user_initials_map.get(str(raw_user), self._to_initials(str(raw_user))),
                "description": self._html_to_text(
                    self._first_non_empty(
                        log,
                        (
                            "description",
                            "subject",
                            "commentcontent",
                            "comments",
                            "details",
                            "note",
                            "notes",
                            "cf_timelog_description",
                        ),
                    )
                ) or "Ingen beskrivelse",
                "status": log.get("status"),
                "billable": log.get("billable"),
                "hours": hours,
                "vtiger_url": self._build_vtiger_record_url("Timelog", log.get("id")),
            }

            if related_case_id in case_map:
                target_case = case_map[related_case_id]
            else:
                if unmatched_case_id not in case_map:
                    unmatched_case = {
                        "id": unmatched_case_id,
                        "cc_number": "-",
                        "title": "Timelogs uden relateret case",
                        "description": "Ingen relateret sag fra servicekontrakten.",
                        "contact_person": "-",
                        "vtiger_url": "",
                        "status": "Unknown",
                        "priority": None,
                        "total_hours": Decimal("0"),
                        "timelog_count": 0,
                        "timelogs": [],
                    }
                    case_map[unmatched_case_id] = unmatched_case
                    report_cases.append(unmatched_case)
                target_case = case_map[unmatched_case_id]

            target_case["timelogs"].append(timelog_payload)
            target_case["timelog_count"] += 1
            target_case["total_hours"] = (target_case["total_hours"] + hours).quantize(Decimal("0.01"))

        total_timelogs = sum(int(case_item["timelog_count"]) for case_item in report_cases)
        total_hours = sum((case_item["total_hours"] for case_item in report_cases), Decimal("0")).quantize(Decimal("0.01"))
        closed_cases = sum(
            1 for case_item in report_cases
            if self._is_closed_case(case_item.get("status"))
        )
        open_cases = max(0, len(report_cases) - closed_cases)

        return {
            "customer": {
                "account_id": safe_account_id,
                "account_name": account_name,
            },
            "contract": {
                "id": safe_contract_id,
                "contract_number": contract.get("contract_number") or contract.get("contract_no") or "",
                "subject": contract.get("subject") or "",
                "contract_status": contract.get("contract_status"),
                "vtiger_url": self._build_vtiger_record_url("ServiceContracts", safe_contract_id),
            },
            "cases": report_cases,
            "summary": {
                "total_cases": len(report_cases),
                "open_cases": open_cases,
                "closed_cases": closed_cases,
                "total_timelogs": total_timelogs,
                "total_hours": total_hours,
            },
        }


# Singleton instance
_vtiger_service = None

def get_vtiger_service() -> VTigerService:
    """Get or create vTiger service singleton"""
    global _vtiger_service
    if _vtiger_service is None:
        _vtiger_service = VTigerService()
    return _vtiger_service
