"""
Main API Router for Time Tracking Module
=========================================

Samler alle endpoints for modulet.
Isoleret routing uden påvirkning af existing Hub endpoints.
"""

import logging
from io import BytesIO
from typing import Optional, List, Dict, Any
from datetime import datetime, date, timedelta
from calendar import monthrange

from fastapi import APIRouter, HTTPException, Depends, Body, Query
from fastapi.responses import JSONResponse, Response

from app.core.database import execute_query, execute_update, execute_query_single
from app.timetracking.backend.models import (
    TModuleSyncStats,
    TModuleApprovalStats,
    TModuleWizardNextEntry,
    TModuleWizardProgress,
    TModuleTimeApproval,
    TModuleTimeWithContext,
    TModuleOrder,
    TModuleOrderWithLines,
    TModuleEconomicExportRequest,
    TModuleEconomicExportResult,
    TModuleMetadata,
    TModuleUninstallRequest,
    TModuleUninstallResult,
    TModuleBulkRateUpdate,
    ServiceContractWizardData,
    ServiceContractWizardAction,
    ServiceContractWizardSummary,
    ServiceContractReportCustomer,
    ServiceContractReportContract,
    ServiceContractReportData,
    TimologTransferRequest,
    TimologTransferResult,
)
from app.timetracking.backend.vtiger_sync import vtiger_service
from app.timetracking.backend.wizard import wizard
from app.timetracking.backend.order_service import order_service
from app.timetracking.backend.economic_export import economic_service
from app.timetracking.backend.audit import audit
from app.services.customer_consistency import CustomerConsistencyService
from app.timetracking.backend.service_contract_wizard import ServiceContractWizardService
from app.services.vtiger_service import get_vtiger_service
from app.ticket.backend.klippekort_service import KlippekortService
from app.core.auth_dependencies import get_optional_user

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/timetracking")


def _resolve_current_user_id(current_user: Optional[dict]) -> Optional[int]:
    if not current_user:
        return None
    raw = current_user.get("id") or current_user.get("user_id")
    try:
        return int(raw) if raw is not None else None
    except (TypeError, ValueError):
        return None


def _resolve_target_user_id(current_user: Optional[dict], payload_user_id: Any = None) -> Optional[int]:
    """Resolve target medarbejder_id, preferring explicit payload value when provided."""
    if payload_user_id not in (None, ""):
        try:
            return int(payload_user_id)
        except (TypeError, ValueError):
            return None
    return _resolve_current_user_id(current_user)


def _parse_iso_datetime(value: Optional[str]) -> Optional[datetime]:
    if not value:
        return None
    if isinstance(value, datetime):
        return value
    normalized = str(value).strip().replace("Z", "+00:00")
    try:
        return datetime.fromisoformat(normalized)
    except ValueError:
        return None


def _minutes_between(start: Optional[datetime], end: Optional[datetime]) -> Optional[int]:
    if not start or not end:
        return None
    diff_seconds = int((end - start).total_seconds())
    return max(0, diff_seconds // 60)


def _seconds_between(start: Optional[datetime], end: Optional[datetime]) -> int:
    if not start or not end:
        return 0
    return max(0, int((end - start).total_seconds()))


def _elapsed_minutes_excluding_pause(entry: Dict[str, Any], end: datetime) -> int:
    start_tid = entry.get("start_tid")
    if not start_tid:
        return 0

    total_seconds = _seconds_between(start_tid, end)
    paused_seconds = int(entry.get("pause_total_seconds") or 0)
    paused_at = entry.get("paused_at")
    if paused_at:
        paused_seconds += _seconds_between(paused_at, end)

    effective_seconds = max(0, total_seconds - paused_seconds)
    return effective_seconds // 60


def _pause_total_seconds_at(entry: Dict[str, Any], end: datetime) -> int:
    paused_seconds = int(entry.get("pause_total_seconds") or 0)
    paused_at = entry.get("paused_at")
    if paused_at:
        paused_seconds += _seconds_between(paused_at, end)
    return max(0, paused_seconds)


def _round_up_minutes(minutes: int, block_minutes: int = 30) -> int:
    safe_minutes = max(0, int(minutes or 0))
    safe_block = max(1, int(block_minutes or 30))
    if safe_minutes == 0:
        return 0
    return ((safe_minutes + safe_block - 1) // safe_block) * safe_block


def _legacy_status_from_entry_status(entry_status: str) -> str:
    if entry_status == "godkendt":
        return "approved"
    if entry_status == "kladde":
        return "pending"
    return "pending"


def _resolve_case_customer_id(sag_id: Any, payload_customer_id: Any = None) -> Optional[int]:
    """Resolve tmodule customer_id for a case (tmodule_times FK target)."""
    try:
        candidate_customer_id = int(payload_customer_id) if payload_customer_id is not None else None
    except (TypeError, ValueError):
        candidate_customer_id = None

    # If payload already points to a tmodule customer id, use it directly.
    if candidate_customer_id is not None:
        direct = execute_query_single(
            "SELECT id FROM tmodule_customers WHERE id = %s",
            (candidate_customer_id,),
        )
        if direct and direct.get("id") is not None:
            return int(direct["id"])

    # Otherwise resolve hub customer id from payload or case.
    hub_customer_id = candidate_customer_id
    if hub_customer_id is None:
        row = execute_query_single(
            """
            SELECT customer_id
            FROM sag_sager
            WHERE id = %s AND deleted_at IS NULL
            """,
            (sag_id,),
        )
        if not row:
            return None
        customer_id = row.get("customer_id")
        try:
            hub_customer_id = int(customer_id) if customer_id is not None else None
        except (TypeError, ValueError):
            hub_customer_id = None

    if hub_customer_id is None:
        return None

    def _get_existing_mapping() -> Optional[int]:
        mapped = execute_query_single(
            """
            SELECT id
            FROM tmodule_customers
            WHERE hub_customer_id = %s
            ORDER BY id ASC
            LIMIT 1
            """,
            (hub_customer_id,),
        )
        if not mapped:
            return None
        try:
            return int(mapped.get("id")) if mapped.get("id") is not None else None
        except (TypeError, ValueError):
            return None

    mapped_id = _get_existing_mapping()
    if mapped_id is not None:
        return mapped_id

    # Auto-link fallback: try targeted linking by economic number/name before failing.
    try:
        hub_customer = execute_query_single(
            """
            SELECT id, name, economic_customer_number
            FROM customers
            WHERE id = %s
            """,
            (hub_customer_id,),
        ) or {}

        economic_number = hub_customer.get("economic_customer_number")
        customer_name = str(hub_customer.get("name") or "").strip()

        if economic_number not in (None, ""):
            execute_update(
                """
                UPDATE tmodule_customers
                SET hub_customer_id = %s,
                    economic_customer_number = COALESCE(economic_customer_number, %s),
                    updated_at = NOW()
                WHERE hub_customer_id IS NULL
                  AND economic_customer_number = %s
                """,
                (hub_customer_id, economic_number, economic_number),
            )

        if customer_name:
            execute_update(
                """
                UPDATE tmodule_customers
                SET hub_customer_id = %s,
                    updated_at = NOW()
                WHERE hub_customer_id IS NULL
                  AND LOWER(TRIM(COALESCE(name, ''))) = LOWER(TRIM(%s))
                """,
                (hub_customer_id, customer_name),
            )

        # Last fallback: run global relink function if available.
        try:
            execute_query("SELECT * FROM link_tmodule_customers_to_hub()")
        except Exception:
            pass
    except Exception as link_error:
        logger.warning("⚠️ Auto-link fallback for hub customer %s failed: %s", hub_customer_id, link_error)

    mapped_id = _get_existing_mapping()
    if mapped_id is not None:
        return mapped_id

    # Last-resort fallback: create a local tmodule customer stub from Hub customer.
    # This keeps timer registration operational even if vTiger sync/linking has not run yet.
    try:
        hub_customer = execute_query_single(
            """
            SELECT id, name, email, economic_customer_number
            FROM customers
            WHERE id = %s
            """,
            (hub_customer_id,),
        ) or {}

        hub_name = str(hub_customer.get("name") or "").strip()
        if hub_name:
            synthetic_vtiger_id = f"HUB-{hub_customer_id}"
            created = execute_query(
                """
                INSERT INTO tmodule_customers (
                    vtiger_id,
                    name,
                    email,
                    hub_customer_id,
                    economic_customer_number,
                    vtiger_data,
                    sync_hash,
                    updated_at,
                    last_synced_at
                ) VALUES (
                    %s, %s, %s, %s, %s,
                    %s::jsonb,
                    %s,
                    NOW(), NOW()
                )
                ON CONFLICT (vtiger_id)
                DO UPDATE SET
                    name = EXCLUDED.name,
                    email = COALESCE(EXCLUDED.email, tmodule_customers.email),
                    hub_customer_id = EXCLUDED.hub_customer_id,
                    economic_customer_number = COALESCE(EXCLUDED.economic_customer_number, tmodule_customers.economic_customer_number),
                    updated_at = NOW(),
                    last_synced_at = NOW()
                RETURNING id
                """,
                (
                    synthetic_vtiger_id,
                    hub_name,
                    hub_customer.get("email"),
                    hub_customer_id,
                    hub_customer.get("economic_customer_number"),
                    '{"source":"hub_fallback"}',
                    f"hub-fallback-{hub_customer_id}",
                ),
            ) or []
            if created and created[0].get("id") is not None:
                return int(created[0]["id"])
    except Exception as create_error:
        logger.warning("⚠️ Could not create tmodule customer fallback for hub customer %s: %s", hub_customer_id, create_error)

    return None


# ============================================================================
# SYNC ENDPOINTS
# ============================================================================

@router.post("/sync", response_model=TModuleSyncStats, tags=["Sync"])
async def sync_from_vtiger(
    user_id: Optional[int] = None,
    fetch_comments: bool = False
):
    """
    🔍 Synkroniser data fra vTiger (READ-ONLY).
    
    Henter:
    - Accounts (kunder)
    - HelpDesk (cases)
    - ModComments (tidsregistreringer)
    
    Gemmes i tmodule_* tabeller (isoleret).
    
    Args:
        user_id: ID på bruger der kører sync
        fetch_comments: Hent også interne kommentarer (langsomt - ~0.4s pr case)
    """
    try:
        logger.info("🚀 Starting vTiger sync...")
        result = await vtiger_service.full_sync(user_id=user_id, fetch_comments=fetch_comments)
        
        # Auto-link customers after sync to update links based on fresh vTiger data
        logger.info("🔗 Auto-linking customers after sync...")
        try:
            link_query = "SELECT * FROM link_tmodule_customers_to_hub()"
            link_results = execute_query(link_query)
            logger.info(f"✅ Linked {len(link_results)} customers automatically")
        except Exception as link_error:
            logger.warning(f"⚠️ Customer linking failed (non-critical): {link_error}")
        
        return result
    except Exception as e:
        logger.error(f"❌ Sync failed: {e}")
        raise HTTPException(status_code=500, detail=str(e))


@router.post("/sync/case/{case_id}/comments", tags=["Sync"])
async def sync_case_comments(case_id: int):
    """
    🔍 Synkroniser kommentarer for en specifik case fra vTiger.
    
    Bruges til on-demand opdatering når man ser på en case i wizard.
    """
    try:
        # Hent case fra database
        case = execute_query_single(
            "SELECT vtiger_id FROM tmodule_cases WHERE id = %s",
            (case_id,))
        
        if not case:
            raise HTTPException(status_code=404, detail="Case not found")
        
        # Sync comments
        result = await vtiger_service.sync_case_comments(case['vtiger_id'])
        
        if not result['success']:
            raise HTTPException(status_code=500, detail=result.get('error', 'Failed to sync comments'))
        
        return result
        
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"❌ Failed to sync comments for case {case_id}: {e}")
        raise HTTPException(status_code=500, detail=str(e))


@router.get("/sync/test-connection", tags=["Sync"])
async def test_vtiger_connection():
    """Test forbindelse til vTiger"""
    try:
        is_connected = await vtiger_service.test_connection()
        return {
            "connected": is_connected,
            "service": "vTiger CRM",
            "read_only": vtiger_service.read_only,
            "dry_run": vtiger_service.dry_run
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@router.post("/sync/fix-empty-case-titles", tags=["Sync"])
async def fix_empty_case_titles():
    """
    🔧 Opdater tomme case titles fra vtiger_data.
    
    Henter titles fra vtiger_data JSON for cases der har tom eller generisk title.
    """
    try:
        logger.info("🔧 Fixing empty case titles...")
        
        # Find cases med tomme eller generiske titles
        cases_query = """
            SELECT id, vtiger_id, title, 
                   vtiger_data->>'title' as vtiger_title,
                   vtiger_data->>'ticket_title' as vtiger_ticket_title
            FROM tmodule_cases
            WHERE title IS NULL 
               OR TRIM(title) = '' 
               OR LOWER(title) IN ('ingen titel', 'no title', 'none')
        """
        
        cases = execute_query(cases_query)
        
        if not cases:
            return {
                "success": True,
                "message": "No cases with empty titles found",
                "updated": 0
            }
        
        updated_count = 0
        for case in cases:
            # Prioriter title fra vtiger_data
            new_title = case.get('vtiger_title') or case.get('vtiger_ticket_title')
            
            if new_title and new_title.strip():
                execute_query(
                    "UPDATE tmodule_cases SET title = %s WHERE id = %s",
                    (new_title, case['id'])
                )
                updated_count += 1
                logger.info(f"✅ Updated case {case['vtiger_id']}: '{new_title}'")
        
        return {
            "success": True,
            "message": f"Updated {updated_count} cases",
            "total_checked": len(cases),
            "updated": updated_count
        }
        
    except Exception as e:
        logger.error(f"❌ Fix empty titles failed: {e}")
        raise HTTPException(status_code=500, detail=str(e))


@router.post("/sync/manual-link-customer", tags=["Sync"])
async def manual_link_customer(tmodule_customer_id: int, hub_customer_id: int):
    """
    🔗 Manually link en tmodule_customer til en Hub customer.
    
    Brug dette når auto-linking fejler pga. navn-forskelle eller andre edge cases.
    """
    try:
        logger.info(f"🔗 Manual linking tmodule {tmodule_customer_id} → Hub {hub_customer_id}")
        
        # Get Hub customer's economic number
        hub_query = "SELECT economic_customer_number FROM customers WHERE id = %s"
        hub_result = execute_query(hub_query, (hub_customer_id,))
        
        if not hub_result:
            raise HTTPException(status_code=404, detail=f"Hub customer {hub_customer_id} not found")
        
        economic_number = hub_result[0]['economic_customer_number']
        
        # Update tmodule_customer
        update_query = """
            UPDATE tmodule_customers 
            SET hub_customer_id = %s,
                economic_customer_number = %s,
                updated_at = NOW()
            WHERE id = %s
            RETURNING id, name, hub_customer_id, economic_customer_number
        """
        
        result = execute_query(update_query, (hub_customer_id, economic_number, tmodule_customer_id))
        
        if not result:
            raise HTTPException(status_code=404, detail=f"tmodule_customer {tmodule_customer_id} not found")
        
        logger.info(f"✅ Manual link complete: {result[0]}")
        
        return {
            "success": True,
            "linked_customer": result[0]
        }
        
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"❌ Manual linking failed: {e}")
        raise HTTPException(status_code=500, detail=str(e))


@router.post("/sync/relink-customers", tags=["Sync"])
async def relink_customers():
    """
    🔗 Re-link alle tmodule_customers til Hub customers.
    
    Bruger linking funktion der prioriterer:
    1. economic_customer_number match (mest præcis)
    2. Navn match (fallback)
    
    Kør efter vTiger sync for at opdatere kunde-links baseret på
    nyeste data fra vTiger (cf_854 economic numbers).
    """
    try:
        logger.info("🔗 Starting customer re-linking...")
        
        # Call database function
        query = "SELECT * FROM link_tmodule_customers_to_hub()"
        results = execute_query(query)
        
        # Count by action type
        stats = {
            "economic_matches": 0,
            "name_matches": 0,
            "total_linked": len(results)
        }
        
        for result in results:
            if result['action'] == 'economic_number_match':
                stats["economic_matches"] += 1
            elif result['action'] == 'name_match':
                stats["name_matches"] += 1
        
        logger.info(f"✅ Customer linking complete: {stats}")
        
        return {
            "success": True,
            "stats": stats,
            "linked_customers": results
        }
        
    except Exception as e:
        logger.error(f"❌ Customer linking failed: {e}")
        raise HTTPException(status_code=500, detail=str(e))


@router.get("/debug/raw-stats", tags=["Debug"])
async def get_debug_raw_stats():
    """
    🔍 DEBUG: Vis rå statistik fra databasen uden filtering.
    
    Bruges til at diagnosticere hvorfor timelogs ikke vises.
    """
    try:
        # Total counts without any filtering
        total_query = """
            SELECT 
                (SELECT COUNT(*) FROM tmodule_customers) as total_customers,
                (SELECT COUNT(*) FROM tmodule_cases) as total_cases,
                (SELECT COUNT(*) FROM tmodule_times) as total_times,
                (SELECT COUNT(*) FROM tmodule_times WHERE billable = true) as billable_times,
                (SELECT COUNT(*) FROM tmodule_times WHERE status = 'pending') as pending_times,
                (SELECT COUNT(*) FROM tmodule_times WHERE vtiger_data->>'cf_timelog_invoiced' = '0') as not_invoiced_times,
                (SELECT COUNT(*) FROM tmodule_times WHERE vtiger_data->>'cf_timelog_invoiced' IS NULL) as null_invoiced_times,
                (SELECT COUNT(*) FROM tmodule_times WHERE billable = true AND status = 'pending') as billable_pending,
                (SELECT COUNT(*) FROM tmodule_times WHERE billable = true AND status = 'pending' AND vtiger_data->>'cf_timelog_invoiced' = '0') as filtered_pending
        """
        totals = execute_query_single(total_query)
        
        # Sample timelogs to see actual data
        sample_query = """
            SELECT 
                id, vtiger_id, description, worked_date, original_hours, 
                status, billable,
                vtiger_data->>'cf_timelog_invoiced' as cf_timelog_invoiced,
                vtiger_data->>'isbillable' as is_billable_field,
                customer_id, case_id
            FROM tmodule_times 
            ORDER BY worked_date DESC
            LIMIT 10
        """
        samples = execute_query(sample_query)
        
        # Check what invoice statuses actually exist
        invoice_status_query = """
            SELECT 
                vtiger_data->>'cf_timelog_invoiced' as invoice_status,
                COUNT(*) as count
            FROM tmodule_times
            GROUP BY vtiger_data->>'cf_timelog_invoiced'
            ORDER BY count DESC
        """
        invoice_statuses = execute_query(invoice_status_query)
        
        return {
            "totals": totals,
            "sample_timelogs": samples,
            "invoice_statuses": invoice_statuses,
            "explanation": {
                "issue": "If not_invoiced_times is 0 but total_times > 0, then the cf_timelog_invoiced field is not '0' in vTiger",
                "solution": "The SQL views filter on cf_timelog_invoiced = '0' but vTiger might use different values",
                "check": "Look at invoice_statuses to see what values actually exist"
            }
        }
    except Exception as e:
        logger.error(f"❌ Debug query failed: {e}")
        raise HTTPException(status_code=500, detail=str(e))


# ============================================================================
# WIZARD / APPROVAL ENDPOINTS
# ============================================================================

@router.get("/wizard/stats", response_model=List[TModuleApprovalStats], tags=["Wizard"])
async def get_all_customer_stats():
    """Hent approval statistik for alle kunder"""
    try:
        return wizard.get_all_customers_stats()
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@router.get("/wizard/next", response_model=TModuleWizardNextEntry, tags=["Wizard"])
async def get_next_pending_entry(
    customer_id: Optional[int] = None,
    exclude_time_card: bool = True
):
    """
    Hent næste pending tidsregistrering til godkendelse.
    
    Query params:
    - customer_id: Filtrer til specifik kunde (optional)
    - exclude_time_card: Ekskluder klippekort-kunder (default: true)
    """
    try:
        return wizard.get_next_pending_entry(
            customer_id=customer_id,
            exclude_time_card=exclude_time_card
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@router.post("/wizard/approve/{time_id}", response_model=TModuleTimeWithContext, tags=["Wizard"])
async def approve_time_entry(
    time_id: int,
    request: Dict[str, Any] = Body(...),
    user_id: Optional[int] = None
):
    """
    Godkend en tidsregistrering.
    
    Path params:
    - time_id: ID på tidsregistreringen
    
    Body:
    - billable_hours: Timer efter godkendelse (optional)
    - hourly_rate: Timepris i DKK (optional)
    - is_travel: True hvis kørsel (optional)
    - approval_note: Godkendelsesnote (optional)
    - rounding_method: "up", "down", "nearest" (optional)
    """
    try:
        from app.core.config import settings
        from decimal import Decimal
        
        # SPECIAL HANDLER FOR HUB WORKLOGS (Negative IDs)
        if time_id < 0:
            worklog_id = abs(time_id)
            logger.info(f"🔄 Approving Hub Worklog {worklog_id}")
            
            w_entry = execute_query_single("SELECT * FROM tticket_worklog WHERE id = %s", (worklog_id,))
            if not w_entry:
                raise HTTPException(status_code=404, detail="Worklog not found")
                
            billable_hours = request.get('billable_hours')
            approved_hours = Decimal(str(billable_hours)) if billable_hours is not None else Decimal(str(w_entry['hours']))
            
            is_billable = request.get('billable', True)
            new_billing = 'invoice' if is_billable else 'internal'
            
            execute_query("""
                UPDATE tticket_worklog 
                SET hours = %s, billing_method = %s, status = 'billable'
                WHERE id = %s
            """, (approved_hours, new_billing, worklog_id))
            
            return {
                "id": time_id,
                "worked_date": w_entry['work_date'],
                "original_hours": w_entry['hours'],
                "status": "approved",
                # Mock fields for schema validation
                "customer_id": 0,
                "case_id": 0,
                "description": w_entry['description'],
                "case_title": "Ticket Worklog",
                "customer_name": "Hub Customer",
                "created_at": w_entry['created_at'],
                "last_synced_at": datetime.now(),
                "approved_hours": approved_hours
            }


        # Hent timelog
        query = """
            SELECT t.*, c.title as case_title, c.status as case_status,
                   cust.name as customer_name, cust.hourly_rate as customer_rate
            FROM tmodule_times t
            JOIN tmodule_cases c ON t.case_id = c.id
            JOIN tmodule_customers cust ON t.customer_id = cust.id
            WHERE t.id = %s
        """
        entry = execute_query_single(query, (time_id,))
        
        if not entry:
            raise HTTPException(status_code=404, detail="Time entry not found")
        
        # Beregn approved_hours
        billable_hours = request.get('billable_hours')
        rounding_method = request.get('rounding_method')
        
        if billable_hours is None:
            approved_hours = Decimal(str(entry['original_hours']))
            
            # Auto-afrund hvis enabled
            if settings.TIMETRACKING_AUTO_ROUND:
                increment = Decimal(str(settings.TIMETRACKING_ROUND_INCREMENT))
                method = rounding_method or settings.TIMETRACKING_ROUND_METHOD
                
                if method == "up":
                    approved_hours = (approved_hours / increment).quantize(Decimal('1'), rounding='ROUND_UP') * increment
                elif method == "down":
                    approved_hours = (approved_hours / increment).quantize(Decimal('1'), rounding='ROUND_DOWN') * increment
                else:
                    approved_hours = (approved_hours / increment).quantize(Decimal('1'), rounding='ROUND_HALF_UP') * increment
                
                rounded_to = increment
            else:
                rounded_to = None
        else:
            approved_hours = Decimal(str(billable_hours))
            rounded_to = None
        
        # Note: hourly_rate is stored on customer level (tmodule_customers.hourly_rate), not on time entries
        # Frontend sends it for calculation display but we don't store it per time entry
        
        # Godkend med alle felter
        logger.info(f"🔍 Creating approval for time_id={time_id}: approved_hours={approved_hours}, rounded_to={rounded_to}, is_travel={request.get('is_travel', False)}, billable={request.get('billable', True)}")
        
        approval = TModuleTimeApproval(
            time_id=time_id,
            approved_hours=approved_hours,
            rounded_to=rounded_to,
            approval_note=request.get('approval_note'),
            billable=request.get('billable', True),  # Accept from request, default til fakturerbar
            is_travel=request.get('is_travel', False)
        )
        
        logger.info(f"✅ Approval object created successfully")
        return wizard.approve_time_entry(approval, user_id=user_id)
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"❌ Error approving entry {time_id}: {e}", exc_info=True)
        raise HTTPException(status_code=500, detail=str(e))


@router.post("/wizard/reject/{time_id}", response_model=TModuleTimeWithContext, tags=["Wizard"])
async def reject_time_entry(
    time_id: int,
    request: Dict[str, Any] = Body(None), # Allow body
    reason: Optional[str] = None,
    user_id: Optional[int] = None
):
    """Afvis en tidsregistrering"""
    try:
        # Handle body extraction if reason is missing from query
        if not reason and request and 'rejection_note' in request:
            reason = request['rejection_note']
            
        if time_id < 0:
            worklog_id = abs(time_id)
            
            # Retrieve to confirm existence
            w = execute_query_single("SELECT * FROM tticket_worklog WHERE id = %s", (worklog_id,))
            if not w:
                raise HTTPException(status_code=404, detail="Entry not found")
                
            execute_query("UPDATE tticket_worklog SET status = 'rejected' WHERE id = %s", (worklog_id,))
            
            return {
                "id": time_id, 
                "status": "rejected",
                "original_hours": w['hours'],
                "worked_date": w['work_date'],
                # Mock fields for schema validation
                "customer_id": 0,
                "case_id": 0,
                "description": w.get('description', ''),
                "case_title": "Ticket Worklog",
                "customer_name": "Hub Customer",
                "created_at": w['created_at'],
                "last_synced_at": datetime.now(),
                "billable": False
            }

        return wizard.reject_time_entry(time_id, reason=reason, user_id=user_id)
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


from app.timetracking.backend.models import TModuleWizardEditRequest

@router.patch("/wizard/entry/{time_id}", response_model=TModuleTimeWithContext, tags=["Wizard"])
async def update_entry_details(
    time_id: int,
    request: TModuleWizardEditRequest
):
    """
    Opdater detaljer på en tidsregistrering (før godkendelse).
    Tillader ændring af beskrivelse, antal timer og faktureringsmetode.
    """
    try:
        from decimal import Decimal
        
        # 1. Handling Hub Worklogs (Negative IDs)
        if time_id < 0:
            worklog_id = abs(time_id)
            w = execute_query_single("SELECT * FROM tticket_worklog WHERE id = %s", (worklog_id,))
            if not w:
                raise HTTPException(status_code=404, detail="Worklog not found")
            
            updates = []
            params = []
            
            if request.description is not None:
                updates.append("description = %s")
                params.append(request.description)
                
            if request.original_hours is not None:
                updates.append("hours = %s")
                params.append(request.original_hours)
                
            if request.billing_method is not None:
                updates.append("billing_method = %s")
                params.append(request.billing_method)
                
            if updates:
                params.append(worklog_id)
                execute_query(f"UPDATE tticket_worklog SET {', '.join(updates)} WHERE id = %s", tuple(params))
                w = execute_query_single("SELECT * FROM tticket_worklog WHERE id = %s", (worklog_id,))

            return {
                "id": time_id,
                "worked_date": w['work_date'],
                "original_hours": w['hours'],
                "status": "pending", # Always return as pending/draft context here
                "description": w['description'],
                "customer_id": 0,
                "case_id": 0, 
                "case_title": "Updated",
                "customer_name": "Hub Customer",
                "created_at": w['created_at'],
                "last_synced_at": datetime.now(),
                "billable": True
            }

        # 2. Handling Module Times (Positive IDs)
        else:
            t = execute_query_single("SELECT * FROM tmodule_times WHERE id = %s", (time_id,))
            if not t:
                raise HTTPException(status_code=404, detail="Time entry not found")
                
            updates = []
            params = []
            
            if request.description is not None:
                updates.append("description = %s")
                params.append(request.description)
                
            if request.original_hours is not None:
                updates.append("original_hours = %s")
                params.append(request.original_hours)
                
            if request.billable is not None:
                updates.append("billable = %s")
                params.append(request.billable)
                
            if updates:
                params.append(time_id)
                execute_query(f"UPDATE tmodule_times SET {', '.join(updates)} WHERE id = %s", tuple(params))
                
            # Fetch fresh with context for response
            query = """
                SELECT t.*, c.title as case_title, c.status as case_status,
                       cust.name as customer_name
                FROM tmodule_times t
                LEFT JOIN tmodule_cases c ON t.case_id = c.id
                LEFT JOIN tmodule_customers cust ON t.customer_id = cust.id
                WHERE t.id = %s
            """
            return execute_query_single(query, (time_id,))

    except Exception as e:
        logger.error(f"❌ Failed to update entry {time_id}: {e}")
        raise HTTPException(status_code=500, detail=str(e))


@router.post("/wizard/reset/{time_id}", response_model=TModuleTimeWithContext, tags=["Wizard"])
async def reset_to_pending(
    time_id: int,
    reason: Optional[str] = None,
    user_id: Optional[int] = None
):
    """
    Nulstil en godkendt/afvist tidsregistrering tilbage til pending.
    
    Query params:
    - reason: Årsag til nulstilling (optional)
    - user_id: ID på brugeren der nulstiller (optional)
    """
    try:
        return wizard.reset_to_pending(time_id, reason=reason, user_id=user_id)
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"❌ Reset failed: {e}")
        raise HTTPException(status_code=500, detail=str(e))


@router.get("/wizard/case/{case_id}/entries", response_model=List[TModuleTimeWithContext], tags=["Wizard"])
async def get_case_entries(
    case_id: int,
    exclude_time_card: bool = True
):
    """
    Hent alle pending timelogs for en case.
    
    Bruges til at vise alle tidsregistreringer i samme case grupperet.
    """
    try:
        return wizard.get_case_entries(case_id, exclude_time_card=exclude_time_card)
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@router.get("/wizard/case/{case_id}/details", tags=["Wizard"])
async def get_case_details(case_id: int):
    """
    Hent komplet case information inkl. alle timelogs og kommentarer.
    
    Returnerer:
    - case_id, case_title, case_description, case_status
    - timelogs: ALLE tidsregistreringer (pending, approved, rejected)
    - case_comments: Kommentarer fra vTiger
    """
    try:
        return wizard.get_case_details(case_id)
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@router.post("/wizard/case/{case_id}/approve-all", tags=["Wizard"])
async def approve_all_case_entries(
    case_id: int,
    user_id: Optional[int] = None,
    exclude_time_card: bool = True
):
    """
    Bulk-godkend alle pending timelogs for en case.
    
    Afr under automatisk efter configured settings.
    """
    try:
        return wizard.approve_case_entries(
            case_id=case_id,
            user_id=user_id,
            exclude_time_card=exclude_time_card
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@router.get("/wizard/progress/{customer_id}", response_model=TModuleWizardProgress, tags=["Wizard"])
async def get_customer_progress(customer_id: int):
    """Hent wizard progress for en kunde"""
    try:
        return wizard.get_customer_progress(customer_id)
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


# ============================================================================
# ORDER ENDPOINTS
# ============================================================================

@router.get("/customers/{customer_id}/data-consistency", tags=["Customers", "Data Consistency"])
async def check_tmodule_customer_data_consistency(customer_id: int):
    """
    🔍 Check data consistency across Hub, vTiger, and e-conomic for tmodule_customer.
    
    Before creating order, verify customer data is in sync across all systems.
    Maps tmodule_customers.hub_customer_id to the consistency service.
    
    Returns discrepancies found between the three systems.
    """
    try:
        from app.core.config import settings
        
        if not settings.AUTO_CHECK_CONSISTENCY:
            return {
                "enabled": False,
                "message": "Data consistency checking is disabled"
            }
        
        # Get tmodule_customer and find linked hub customer
        tmodule_customer = execute_query_single(
            "SELECT * FROM tmodule_customers WHERE id = %s",
            (customer_id,)
        )
        
        if not tmodule_customer:
            raise HTTPException(status_code=404, detail=f"Customer {customer_id} not found in tmodule_customers")
        
        # Get linked hub customer ID
        hub_customer_id = tmodule_customer.get('hub_customer_id')
        
        if not hub_customer_id:
            return {
                "enabled": True,
                "customer_id": customer_id,
                "discrepancy_count": 0,
                "discrepancies": {},
                "systems_available": {
                    "hub": False,
                    "vtiger": bool(tmodule_customer.get('vtiger_id')),
                    "economic": False
                },
                "message": "Customer not linked to Hub - cannot check consistency"
            }
        
        # Use Hub customer ID for consistency check
        consistency_service = CustomerConsistencyService()
        
        # Fetch data from all systems
        all_data = await consistency_service.fetch_all_data(hub_customer_id)
        
        # Compare data
        discrepancies = consistency_service.compare_data(all_data)
        
        # Count actual discrepancies
        discrepancy_count = sum(
            1 for field_data in discrepancies.values() 
            if field_data['discrepancy']
        )
        
        return {
            "enabled": True,
            "customer_id": customer_id,
            "hub_customer_id": hub_customer_id,
            "discrepancy_count": discrepancy_count,
            "discrepancies": discrepancies,
            "systems_available": {
                "hub": True,
                "vtiger": all_data.get('vtiger') is not None,
                "economic": all_data.get('economic') is not None
            }
        }
        
    except Exception as e:
        logger.error(f"❌ Failed to check consistency for tmodule_customer {customer_id}: {e}")
        raise HTTPException(status_code=500, detail=str(e))


@router.post("/customers/{customer_id}/sync-field", tags=["Customers", "Data Consistency"])
async def sync_tmodule_customer_field(
    customer_id: int,
    field_name: str = Body(..., description="Hub field name to sync"),
    source_system: str = Body(..., description="Source system: hub, vtiger, or economic"),
    source_value: str = Body(..., description="The correct value to sync")
):
    """
    🔄 Sync a single field across all systems for tmodule_customer.
    
    Takes the correct value from one system and updates the others.
    Maps tmodule_customers.hub_customer_id to the consistency service.
    """
    try:
        from app.core.config import settings
        
        # Validate source system
        if source_system not in ['hub', 'vtiger', 'economic']:
            raise HTTPException(
                status_code=400, 
                detail=f"Invalid source_system: {source_system}. Must be hub, vtiger, or economic"
            )
        
        # Get tmodule_customer and find linked hub customer
        tmodule_customer = execute_query_single(
            "SELECT * FROM tmodule_customers WHERE id = %s",
            (customer_id,)
        )
        
        if not tmodule_customer:
            raise HTTPException(status_code=404, detail=f"Customer {customer_id} not found in tmodule_customers")
        
        # Get linked hub customer ID
        hub_customer_id = tmodule_customer.get('hub_customer_id')
        
        if not hub_customer_id:
            raise HTTPException(
                status_code=400, 
                detail="Customer not linked to Hub - cannot sync fields"
            )
        
        consistency_service = CustomerConsistencyService()
        
        # Perform sync on the linked Hub customer
        results = await consistency_service.sync_field(
            customer_id=hub_customer_id,
            field_name=field_name,
            source_system=source_system,
            source_value=source_value
        )
        
        logger.info(f"✅ Field '{field_name}' synced from {source_system}: {results}")
        
        return {
            "success": True,
            "field": field_name,
            "source": source_system,
            "value": source_value,
            "results": results
        }
        
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
    except Exception as e:
        logger.error(f"❌ Failed to sync field for tmodule_customer {customer_id}: {e}")
        raise HTTPException(status_code=500, detail=str(e))


@router.post("/orders/generate/{customer_id}", response_model=TModuleOrderWithLines, tags=["Orders"])
async def generate_order(customer_id: int, user_id: Optional[int] = None):
    """
    Generer ordre for alle godkendte tider for en kunde.
    
    Aggregerer:
    - Alle godkendte tidsregistreringer
    - Grupperet efter case
    - Beregner totals med moms
    
    Markerer tidsregistreringer som 'billed'.
    """
    try:
        return order_service.generate_order_for_customer(customer_id, user_id=user_id)
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@router.get("/orders", response_model=List[TModuleOrder], tags=["Orders"])
async def list_orders(
    customer_id: Optional[int] = None,
    status: Optional[str] = None,
    limit: int = 100
):
    """
    List ordrer med filtrering.
    
    Query params:
    - customer_id: Filtrer til specifik kunde
    - status: Filtrer på status (draft, exported, sent, cancelled)
    - limit: Max antal resultater (default: 100)
    """
    try:
        return order_service.list_orders(
            customer_id=customer_id,
            status=status,
            limit=limit
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@router.get("/orders/{order_id}", response_model=TModuleOrderWithLines, tags=["Orders"])
async def get_order(order_id: int):
    """Hent ordre med linjer"""
    try:
        return order_service.get_order_with_lines(order_id)
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@router.post("/orders/{order_id}/cancel", response_model=TModuleOrder, tags=["Orders"])
async def cancel_order(
    order_id: int,
    reason: Optional[str] = None,
    user_id: Optional[int] = None
):
    """
    Annuller en ordre.
    
    Kun muligt for draft orders (ikke exported).
    Resetter tidsregistreringer tilbage til 'approved'.
    """
    try:
        return order_service.cancel_order(order_id, reason=reason, user_id=user_id)
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@router.post("/orders/{order_id}/unlock", tags=["Orders"])
async def unlock_order(
    order_id: int,
    admin_code: str,
    user_id: Optional[int] = None
):
    """
    🔓 Lås en eksporteret ordre op for ændringer (ADMIN ONLY).
    
    Kræver:
    1. Korrekt admin unlock code (fra TIMETRACKING_ADMIN_UNLOCK_CODE)
    2. Ordren skal være slettet fra e-conomic først
    
    Query params:
    - admin_code: Admin unlock kode
    """
    try:
        from app.core.config import settings
        
        # Verify admin code
        if not settings.TIMETRACKING_ADMIN_UNLOCK_CODE:
            raise HTTPException(
                status_code=500,
                detail="Admin unlock code ikke konfigureret i systemet"
            )
        
        if admin_code != settings.TIMETRACKING_ADMIN_UNLOCK_CODE:
            logger.warning(f"⚠️ Ugyldig unlock code forsøg for ordre {order_id}")
            raise HTTPException(status_code=403, detail="Ugyldig admin kode")
        
        # Get order
        order = order_service.get_order_with_lines(order_id)
        
        if order.status != 'exported':
            raise HTTPException(
                status_code=400,
                detail="Kun eksporterede ordrer kan låses op"
            )
        
        if not order.economic_draft_id:
            raise HTTPException(
                status_code=400,
                detail="Ordre har ingen e-conomic ID"
            )
        
        # Check if order still exists in e-conomic
        try:
            draft_exists = await economic_service.check_draft_exists(order.economic_draft_id)
            
            if draft_exists:
                raise HTTPException(
                    status_code=400,
                    detail=f"⚠️ Ordren findes stadig i e-conomic (Draft #{order.economic_draft_id}). Slet den i e-conomic først!"
                )
        except Exception as e:
            logger.error(f"❌ Kunne ikke tjekke e-conomic status: {e}")
            raise HTTPException(
                status_code=500,
                detail=f"Kunne ikke verificere e-conomic status: {str(e)}"
            )
        
        # Unlock order - set status back to draft
        update_query = """
            UPDATE tmodule_orders
            SET status = 'draft',
                economic_draft_id = NULL,
                exported_at = NULL
            WHERE id = %s
            RETURNING *
        """
        
        result = execute_query_single(update_query, (order_id,))
        
        # Log unlock
        audit.log_event(
            event_type="order_unlocked",
            entity_type="order",
            entity_id=order_id,
            user_id=user_id,
            details={
                "previous_economic_id": order.economic_draft_id,
                "unlocked_by_admin": True
            }
        )
        
        logger.info(f"🔓 Order {order_id} unlocked by admin (user {user_id})")
        
        return {
            "success": True,
            "message": "Ordre låst op - kan nu redigeres eller slettes",
            "order_id": order_id
        }
        
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"❌ Unlock failed: {e}")
        raise HTTPException(status_code=500, detail=str(e))


# ============================================================================
# e-conomic EXPORT ENDPOINTS
# ============================================================================

@router.post("/export", response_model=TModuleEconomicExportResult, tags=["Export"])
async def export_to_economic(
    request: TModuleEconomicExportRequest,
    user_id: Optional[int] = None
):
    """
    🚨 Eksporter ordre til e-conomic som draft order.
    
    SAFETY FLAGS:
    - TIMETRACKING_ECONOMIC_READ_ONLY (default: True)
    - TIMETRACKING_ECONOMIC_DRY_RUN (default: True)
    
    Hvis begge er enabled, køres kun dry-run simulation.
    
    Body:
    - order_id: ID på ordren
    - force: Re-eksporter selvom allerede eksporteret (default: false)
    """
    try:
        return await economic_service.export_order(request, user_id=user_id)
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@router.get("/export/test-connection", tags=["Export"])
async def test_economic_connection():
    """Test forbindelse til e-conomic"""
    try:
        is_connected = await economic_service.test_connection()
        return {
            "connected": is_connected,
            "service": "e-conomic",
            "read_only": economic_service.read_only,
            "dry_run": economic_service.dry_run,
            "export_type": economic_service.export_type
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


# ============================================================================
# MODULE METADATA & ADMIN ENDPOINTS
# ============================================================================

@router.get("/metadata", response_model=TModuleMetadata, tags=["Admin"])
async def get_module_metadata():
    """Hent modul metadata"""
    try:
        result = execute_query_single(
            "SELECT * FROM tmodule_metadata ORDER BY id DESC LIMIT 1")
        
        if not result:
            raise HTTPException(status_code=404, detail="Module metadata not found")
        
        return TModuleMetadata(**result)
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@router.get("/health", tags=["Admin"])
async def module_health():
    """Module health check"""
    try:
        # Check database tables exist
        tables_query = """
            SELECT COUNT(*) as count FROM information_schema.tables 
            WHERE table_name LIKE 'tmodule_%'
        """
        result = execute_query_single(tables_query)
        table_count = result['count'] if result else 0
        
        # Get stats - count each table separately
        try:
            stats = {
                "customers": 0,
                "cases": 0,
                "times": 0,
                "orders": 0
            }
            
            for table_name in ["customers", "cases", "times", "orders"]:
                count_result = execute_query_single(
                    f"SELECT COUNT(*) as count FROM tmodule_{table_name}")
                stats[table_name] = count_result['count'] if count_result else 0
                
        except Exception as e:
            stats = {"error": str(e)}
        
        return {
            "status": "healthy" if table_count >= 6 else "degraded",
            "module": "Time Tracking & Billing",
            "version": "1.0.0",
            "tables": table_count,
            "statistics": stats,
            "safety": {
                "vtiger_read_only": vtiger_service.read_only,
                "vtiger_dry_run": vtiger_service.dry_run,
                "economic_read_only": economic_service.read_only,
                "economic_dry_run": economic_service.dry_run
            }
        }
    except Exception as e:
        logger.error(f"Health check error: {e}")
        return JSONResponse(
            status_code=500,
            content={"status": "error", "message": str(e)}
        )


@router.get("/config", tags=["Admin"])
async def get_config():
    """Hent modul konfiguration"""
    from app.core.config import settings
    
    return {
        "default_hourly_rate": float(settings.TIMETRACKING_DEFAULT_HOURLY_RATE),
        "auto_round": settings.TIMETRACKING_AUTO_ROUND,
        "round_increment": float(settings.TIMETRACKING_ROUND_INCREMENT),
        "round_method": settings.TIMETRACKING_ROUND_METHOD,
        "vtiger_read_only": settings.TIMETRACKING_VTIGER_READ_ONLY,
        "vtiger_dry_run": settings.TIMETRACKING_VTIGER_DRY_RUN,
        "economic_read_only": settings.TIMETRACKING_ECONOMIC_READ_ONLY,
        "economic_dry_run": settings.TIMETRACKING_ECONOMIC_DRY_RUN
    }


# ============================================================================
# CUSTOMER MANAGEMENT ENDPOINTS
# ============================================================================

@router.patch("/customers/{customer_id}/hourly-rate", tags=["Customers"])
async def update_customer_hourly_rate(customer_id: int, hourly_rate: float, user_id: Optional[int] = None):
    """
    Opdater timepris for en kunde.
    
    Args:
        customer_id: Kunde ID
        hourly_rate: Ny timepris i DKK (f.eks. 850.00)
    """
    try:
        from decimal import Decimal
        
        # Validate rate
        if hourly_rate < 0:
            raise HTTPException(status_code=400, detail="Hourly rate must be positive")
        
        rate_decimal = Decimal(str(hourly_rate))
        
        # Update customer hourly rate
        execute_update(
            "UPDATE tmodule_customers SET hourly_rate = %s, updated_at = CURRENT_TIMESTAMP WHERE id = %s",
            (rate_decimal, customer_id)
        )
        
        # Audit log
        audit.log_event(
            entity_type="customer",
            entity_id=str(customer_id),
            event_type="hourly_rate_updated",
            details={"hourly_rate": float(hourly_rate)},
            user_id=user_id
        )
        
        # Return updated customer
        customer = execute_query_single(
            "SELECT id, name, hourly_rate FROM tmodule_customers WHERE id = %s",
            (customer_id,))
        
        if not customer:
            raise HTTPException(status_code=404, detail="Customer not found")
        
        return {
            "customer_id": customer_id,
            "name": customer['name'],
            "hourly_rate": float(customer['hourly_rate']) if customer['hourly_rate'] else None,
            "updated": True
        }
        
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"❌ Error updating hourly rate: {e}")
        raise HTTPException(status_code=500, detail=str(e))


@router.post("/customers/bulk-update-rate", tags=["Customers"])
async def bulk_update_customer_hourly_rates(
    request: TModuleBulkRateUpdate,
    user_id: Optional[int] = None
):
    """
    Opdater timepris for flere kunder på én gang.
    
    Request body:
        customer_ids: Liste af kunde-ID'er
        hourly_rate: Ny timepris i DKK (f.eks. 850.00)
    
    Returns:
        Antal opdaterede kunder
    """
    try:
        # Validate inputs (already validated by Pydantic)
        customer_ids = request.customer_ids
        rate_decimal = request.hourly_rate
        
        # Update all selected customers
        execute_update(
            "UPDATE tmodule_customers SET hourly_rate = %s, updated_at = CURRENT_TIMESTAMP WHERE id = ANY(%s)",
            (rate_decimal, customer_ids)
        )
        
        # Count affected rows
        updated_count = len(customer_ids)
        
        # Audit log for each customer
        for customer_id in customer_ids:
            audit.log_event(
                entity_type="customer",
                entity_id=str(customer_id),
                event_type="hourly_rate_updated",
                details={"hourly_rate": float(rate_decimal), "bulk_update": True},
                user_id=user_id
            )
        
        logger.info(f"✅ Bulk updated hourly rate for {updated_count} customers to {rate_decimal} DKK")
        
        return {
            "updated": updated_count,
            "hourly_rate": float(rate_decimal)
        }
        
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"❌ Error in bulk hourly rate update: {e}")
        raise HTTPException(status_code=500, detail=str(e))


@router.patch("/customers/{customer_id}/time-card", tags=["Customers"])
async def toggle_customer_time_card(customer_id: int, enabled: bool, user_id: Optional[int] = None):
    """
    Skift klippekort-status for kunde.
    
    Klippekort-kunder faktureres eksternt og skal kunne skjules i godkendelsesflow.
    """
    try:
        # Update customer time card flag
        execute_update(
            "UPDATE tmodule_customers SET uses_time_card = %s, updated_at = CURRENT_TIMESTAMP WHERE id = %s",
            (enabled, customer_id)
        )
        
        # Audit log
        audit.log_event(
            entity_type="customer",
            entity_id=str(customer_id),
            event_type="time_card_toggled",
            details={"enabled": enabled},
            user_id=user_id
        )
        
        # Return updated customer
        customer = execute_query_single(
            "SELECT * FROM tmodule_customers WHERE id = %s",
            (customer_id,))
        
        if not customer:
            raise HTTPException(status_code=404, detail="Customer not found")
        
        return {
            "customer_id": customer_id,
            "name": customer['name'],
            "uses_time_card": customer['uses_time_card'],
            "updated": True
        }
        
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"❌ Error toggling time card: {e}")
        raise HTTPException(status_code=500, detail=str(e))


@router.get("/customers", tags=["Customers"])
async def list_customers(
    include_time_card: bool = True,
    only_with_entries: bool = False
):
    """
    List kunder med filtrering.
    
    Query params:
    - include_time_card: Inkluder klippekort-kunder (default: true)
    - only_with_entries: Kun kunder med pending tidsregistreringer (default: false)
    """
    try:
        if only_with_entries:
            # Use view that includes entry counts
            query = """
                SELECT customer_id, customer_name, customer_vtiger_id, uses_time_card,
                       total_entries, pending_count
                FROM tmodule_approval_stats
                WHERE total_entries > 0
            """
            
            if not include_time_card:
                query += " AND uses_time_card = false"
            
            query += " ORDER BY customer_name"
            
            customers = execute_query_single(query)
        else:
            # Simple customer list
            query = "SELECT * FROM tmodule_customers"
            
            if not include_time_card:
                query += " WHERE uses_time_card = false"
            
            query += " ORDER BY name"
            
            customers = execute_query(query)
        
        return {"customers": customers, "total": len(customers)}
            
    except Exception as e:
        logger.error(f"❌ Error listing customers: {e}")
        raise HTTPException(status_code=500, detail=str(e))


@router.get("/customers/{customer_id}/times", tags=["Customers"])
async def get_customer_time_entries(
    customer_id: int,
    status: Optional[str] = None,
    hub_only: bool = Query(True),
):
    """
    Hent alle tidsregistreringer for en kunde.
    
    Path params:
    - customer_id: Kunde ID
    
    Query params:
    - status: Filtrer på status (pending, approved, rejected, billed)
    """
    try:
        query = """
            SELECT t.*,
                   COALESCE(c.vtiger_data->>'case_no', c.title)::VARCHAR(500) AS case_title,
                   c.vtiger_id AS case_vtiger_id,
                   c.description AS case_description,
                   c.priority AS case_priority,
                   c.module_type AS case_type,
                   cust.name AS customer_name
            FROM tmodule_times t
            LEFT JOIN tmodule_cases c ON t.case_id = c.id
            LEFT JOIN tmodule_customers cust ON t.customer_id = cust.id
            WHERE t.customer_id = %s
              AND t.billed_via_thehub_id IS NULL
              AND (t.vtiger_data->>'cf_timelog_invoiced' IS NULL 
                   OR t.vtiger_data->>'cf_timelog_invoiced' = '0'
                   OR t.vtiger_data->>'cf_timelog_invoiced' = '')
              AND (t.vtiger_data->>'cf_timelog_rounduptimespent' IS NULL 
                   OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '0'
                   OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '')
              AND (t.vtiger_data->>'cf_timelog_billedviathehubid' IS NULL 
                   OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '0'
                   OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '')
        """
        
        params = [customer_id]

        # Hub-only mode: hide imported vTiger timelogs and show only Hub-created entries.
        if hub_only:
            query += " AND t.vtiger_id IS NULL"
        
        if status:
            query += " AND t.status = %s"
            params.append(status)
        
        query += " ORDER BY t.worked_date DESC, t.id DESC"
        
        times = execute_query(query, tuple(params))
        
        # 🔗 Combine with Hub Worklogs (tticket_worklog)
        # Only if we can find the linked Hub Customer ID
        try:
            cust_res = execute_query_single(
                "SELECT hub_customer_id, name FROM tmodule_customers WHERE id = %s", 
                (customer_id,)
            )
            
            if cust_res and cust_res.get('hub_customer_id'):
                hub_id = cust_res['hub_customer_id']
                hub_name = cust_res['name']
                
                # Fetch worklogs
                w_query = """
                    SELECT 
                        (w.id * -1) as id,
                        w.work_date as worked_date,
                        w.hours as original_hours,
                        w.description,
                        CASE 
                            WHEN w.status = 'draft' THEN 'pending'
                            ELSE w.status 
                        END as status,
                        
                        -- Ticket info as Case info
                        t.subject as case_title,
                        t.ticket_number as case_vtiger_id,
                        t.description as case_description,
                        CASE 
                            WHEN t.priority = 'urgent' THEN 'Høj'
                            ELSE 'Normal'
                        END as case_priority,
                        w.work_type as case_type,
                        
                        -- Customer info
                        %s as customer_name,
                        %s as customer_id,
                        
                        -- Logic
                        CASE 
                            WHEN w.billing_method IN ('internal', 'warranty') THEN false 
                            ELSE true 
                        END as billable,
                        false as is_travel,
                        
                        -- Extra context for frontend flags if needed
                        w.billing_method as _billing_method
                        
                    FROM tticket_worklog w
                    JOIN tticket_tickets t ON w.ticket_id = t.id
                    WHERE t.customer_id = %s
                """
                w_params = [hub_name, customer_id, hub_id]
                
                if status:
                    if status == 'pending':
                        w_query += " AND w.status = 'draft'"
                    else:
                        w_query += " AND w.status = %s"
                        w_params.append(status)
                
                w_times = execute_query(w_query, tuple(w_params))
                
                if w_times:
                    times.extend(w_times)
                    # Re-sort combined list
                    times.sort(key=lambda x: (x.get('worked_date') or '', x.get('id')), reverse=True)
                    
        except Exception as e:
            logger.error(f"⚠️ Failed to fetch hub worklogs for wizard: {e}")
            # Continue with just tmodule times
        
        return {"times": times, "total": len(times)}
    
    except Exception as e:
        logger.error(f"Error fetching customer time entries: {e}")
        raise HTTPException(status_code=500, detail=str(e))


@router.get("/times", tags=["Times"])
async def list_time_entries(
    limit: int = 100,
    offset: int = 0,
    status: Optional[str] = None,
    customer_id: Optional[int] = None,
    user_name: Optional[str] = None,
    search: Optional[str] = None
):
    """
    Hent liste af tidsregistreringer med filtre.
    """
    try:
        query = """
            SELECT t.*,
                   COALESCE(c.vtiger_data->>'case_no', c.title)::VARCHAR(500) AS case_title,
                   c.priority AS case_priority,
                   cust.name AS customer_name
            FROM tmodule_times t
            LEFT JOIN tmodule_cases c ON t.case_id = c.id
            LEFT JOIN tmodule_customers cust ON t.customer_id = cust.id
            WHERE 1=1
        """
        params = []
        
        if status:
            query += " AND t.status = %s"
            params.append(status)
            
        if customer_id:
            query += " AND t.customer_id = %s"
            params.append(customer_id)

        if user_name:
            query += " AND t.user_name ILIKE %s"
            params.append(f"%{user_name}%")
            
        if search:
            query += """ AND (
                t.description ILIKE %s OR 
                cust.name ILIKE %s OR
                c.title ILIKE %s
            )"""
            wildcard = f"%{search}%"
            params.extend([wildcard, wildcard, wildcard])
            
        query += " ORDER BY t.worked_date DESC, t.id DESC LIMIT %s OFFSET %s"
        params.extend([limit, offset])
        
        times = execute_query(query, tuple(params))
        return {"times": times}
    except Exception as e:
        logger.error(f"Error listing times: {e}")
        raise HTTPException(status_code=500, detail=str(e))


@router.get("/employee-log/overview", tags=["Times"])
async def get_employee_log_overview(
    granularity: str = Query("week", pattern="^(day|week|month)$"),
    start_date: Optional[date] = Query(None),
    end_date: Optional[date] = Query(None),
    target_daily_hours: float = Query(7.5, ge=0, le=24),
):
    """Employee time overview with gap detection for day/week/month buckets."""
    try:
        today = date.today()
        if not end_date:
            end_date = today

        if not start_date:
            if granularity == "day":
                start_date = end_date - timedelta(days=13)
            elif granularity == "week":
                start_date = end_date - timedelta(days=55)
            else:
                start_date = end_date.replace(day=1) - timedelta(days=150)

        if start_date > end_date:
            raise HTTPException(status_code=400, detail="start_date must be before end_date")

        query = """
            SELECT
                COALESCE(t.medarbejder_id::text, NULLIF(t.user_name, ''), 'unknown') AS employee_key,
                COALESCE(NULLIF(u.full_name, ''), NULLIF(u.username, ''), NULLIF(t.user_name, ''), 'Ukendt') AS employee_name,
                DATE(COALESCE(t.start_tid, t.worked_date::timestamp, t.created_at)) AS work_day,
                SUM(COALESCE(t.original_hours, 0))::numeric AS total_hours,
                COUNT(*)::int AS entry_count,
                COUNT(DISTINCT t.case_id)::int AS case_count,
                SUM(CASE WHEN t.case_id IS NULL THEN 1 ELSE 0 END)::int AS missing_case_entries
            FROM tmodule_times t
            LEFT JOIN users u ON u.user_id = t.medarbejder_id
            WHERE DATE(COALESCE(t.start_tid, t.worked_date::timestamp, t.created_at)) BETWEEN %s AND %s
            GROUP BY employee_key, employee_name, work_day
            ORDER BY employee_name, work_day
        """
        rows = execute_query(query, (start_date, end_date)) or []

        def bucket_start(day: date) -> date:
            if granularity == "day":
                return day
            if granularity == "week":
                return day - timedelta(days=day.weekday())
            return day.replace(day=1)

        def next_bucket_start(day: date) -> date:
            if granularity == "day":
                return day + timedelta(days=1)
            if granularity == "week":
                return day + timedelta(days=7)
            year = day.year + (1 if day.month == 12 else 0)
            month = 1 if day.month == 12 else day.month + 1
            return date(year, month, 1)

        def working_days_between(start: date, end: date) -> int:
            count = 0
            cursor = start
            while cursor <= end:
                if cursor.weekday() < 5:
                    count += 1
                cursor += timedelta(days=1)
            return count

        first_bucket = bucket_start(start_date)
        periods: List[Dict[str, Any]] = []
        cursor = first_bucket
        while cursor <= end_date:
            bucket_end = next_bucket_start(cursor) - timedelta(days=1)
            label = cursor.strftime("%Y-%m-%d")
            if granularity == "week":
                label = f"Uge {cursor.isocalendar().week} ({cursor.strftime('%d/%m')})"
            elif granularity == "month":
                label = cursor.strftime("%b %Y")

            periods.append({
                "key": cursor.isoformat(),
                "label": label,
                "start_date": cursor.isoformat(),
                "end_date": min(bucket_end, end_date).isoformat(),
            })
            cursor = next_bucket_start(cursor)

        employees: Dict[str, Dict[str, Any]] = {}
        for row in rows:
            work_day = row.get("work_day")
            if not work_day:
                continue
            if isinstance(work_day, str):
                work_day = date.fromisoformat(work_day)

            period_key = bucket_start(work_day).isoformat()
            employee_key = row.get("employee_key") or "unknown"
            employee_name = row.get("employee_name") or "Ukendt"

            if employee_key not in employees:
                employees[employee_key] = {
                    "employee_key": employee_key,
                    "employee_name": employee_name,
                    "periods": {},
                    "total_hours": 0.0,
                    "total_entries": 0,
                    "total_cases": 0,
                    "missing_case_entries": 0,
                }

            employee_row = employees[employee_key]
            if period_key not in employee_row["periods"]:
                employee_row["periods"][period_key] = {
                    "hours": 0.0,
                    "entries": 0,
                    "cases": 0,
                    "missing_case_entries": 0,
                }

            period_obj = employee_row["periods"][period_key]
            hours = float(row.get("total_hours") or 0)
            entries = int(row.get("entry_count") or 0)
            cases = int(row.get("case_count") or 0)
            missing_cases = int(row.get("missing_case_entries") or 0)

            period_obj["hours"] += hours
            period_obj["entries"] += entries
            period_obj["cases"] += cases
            period_obj["missing_case_entries"] += missing_cases

            employee_row["total_hours"] += hours
            employee_row["total_entries"] += entries
            employee_row["total_cases"] += cases
            employee_row["missing_case_entries"] += missing_cases

        for employee in employees.values():
            missing_periods = 0
            for period in periods:
                p_start = date.fromisoformat(period["start_date"])
                p_end = date.fromisoformat(period["end_date"])
                workdays = working_days_between(p_start, p_end)
                expected = round(workdays * target_daily_hours, 2)

                obj = employee["periods"].get(period["key"], {
                    "hours": 0.0,
                    "entries": 0,
                    "cases": 0,
                    "missing_case_entries": 0,
                })
                gap_hours = round(max(expected - obj["hours"], 0.0), 2)
                status = "ok"
                if expected > 0 and obj["hours"] == 0:
                    status = "missing"
                elif gap_hours >= 2:
                    status = "warning"

                if status != "ok":
                    missing_periods += 1

                obj.update({
                    "expected_hours": expected,
                    "gap_hours": gap_hours,
                    "status": status,
                })
                employee["periods"][period["key"]] = obj

            employee["missing_periods"] = missing_periods
            employee["completeness_percent"] = 0.0 if not periods else round(
                (len(periods) - missing_periods) / len(periods) * 100,
                1,
            )

        result = sorted(employees.values(), key=lambda e: (e["employee_name"] or "").lower())
        return {
            "granularity": granularity,
            "start_date": start_date.isoformat(),
            "end_date": end_date.isoformat(),
            "target_daily_hours": target_daily_hours,
            "periods": periods,
            "employees": result,
        }
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"❌ Error building employee log overview: {e}")
        raise HTTPException(status_code=500, detail="Kunne ikke hente medarbejder log")


@router.get("/times/{time_id}", tags=["Times"])
async def get_time_entry(time_id: int):
    """
    Hent en specifik tidsregistrering.
    
    Path params:
    - time_id: Time entry ID
    """
    try:
        query = """
            SELECT t.*,
                   COALESCE(c.vtiger_data->>'case_no', c.title)::VARCHAR(500) AS case_title,
                   c.vtiger_id AS case_vtiger_id,
                   c.description AS case_description,
                   cust.name AS customer_name
            FROM tmodule_times t
            LEFT JOIN tmodule_cases c ON t.case_id = c.id
            LEFT JOIN tmodule_customers cust ON t.customer_id = cust.id
            WHERE t.id = %s
        """
        
        time_entry = execute_query_single(query, (time_id,))
        
        if not time_entry:
            raise HTTPException(status_code=404, detail="Time entry not found")
        
        return time_entry
        
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error fetching time entry {time_id}: {e}")
        raise HTTPException(status_code=500, detail=str(e))
        
    except Exception as e:
        logger.error(f"❌ Error getting customer time entries: {e}")
        raise HTTPException(status_code=500, detail=str(e))


@router.delete("/admin/uninstall", response_model=TModuleUninstallResult, tags=["Admin"])
async def uninstall_module(
    request: TModuleUninstallRequest,
    user_id: Optional[int] = None
):
    """
    🚨 SLET MODULET FULDSTÆNDIGT.
    
    ADVARSEL: Dette sletter ALLE data i modulet!
    Kan ikke fortrydes.
    
    Body:
    - confirm: SKAL være true
    - delete_all_data: SKAL være true
    """
    try:
        # Validate request (Pydantic validators already check this)
        if not request.confirm or not request.delete_all_data:
            raise HTTPException(
                status_code=400,
                detail="You must confirm uninstall and data deletion"
            )
        
        logger.warning(f"⚠️ UNINSTALLING TIME TRACKING MODULE (user_id: {user_id})")
        
        # Log uninstall
        audit.log_module_uninstalled(user_id=user_id)
        
        # Execute DROP script
        uninstall_script = """
        DROP VIEW IF EXISTS tmodule_order_details CASCADE;
        DROP VIEW IF EXISTS tmodule_next_pending CASCADE;
        DROP VIEW IF EXISTS tmodule_approval_stats CASCADE;
        
        DROP TRIGGER IF EXISTS tmodule_orders_generate_number ON tmodule_orders;
        DROP TRIGGER IF EXISTS tmodule_orders_update ON tmodule_orders;
        DROP TRIGGER IF EXISTS tmodule_times_update ON tmodule_times;
        DROP TRIGGER IF EXISTS tmodule_cases_update ON tmodule_cases;
        DROP TRIGGER IF EXISTS tmodule_customers_update ON tmodule_customers;
        
        DROP FUNCTION IF EXISTS tmodule_generate_order_number() CASCADE;
        DROP FUNCTION IF EXISTS tmodule_update_timestamp() CASCADE;
        
        DROP TABLE IF EXISTS tmodule_sync_log CASCADE;
        DROP TABLE IF EXISTS tmodule_order_lines CASCADE;
        DROP TABLE IF EXISTS tmodule_orders CASCADE;
        DROP TABLE IF EXISTS tmodule_times CASCADE;
        DROP TABLE IF EXISTS tmodule_cases CASCADE;
        DROP TABLE IF EXISTS tmodule_customers CASCADE;
        DROP TABLE IF EXISTS tmodule_metadata CASCADE;
        """
        
        # Count rows before deletion
        try:
            count_query = """
                SELECT 
                    (SELECT COUNT(*) FROM tmodule_customers) +
                    (SELECT COUNT(*) FROM tmodule_cases) +
                    (SELECT COUNT(*) FROM tmodule_times) +
                    (SELECT COUNT(*) FROM tmodule_orders) +
                    (SELECT COUNT(*) FROM tmodule_order_lines) +
                    (SELECT COUNT(*) FROM tmodule_sync_log) as total
            """
            count_result = execute_query(count_query)
            total_rows = count_result['total'] if count_result else 0
        except:
            total_rows = 0
        
        # Execute uninstall (split into separate statements)
        from app.core.database import get_db_connection
        import psycopg2
        
        conn = get_db_connection()
        cursor = conn.cursor()
        
        dropped_items = {
            "views": [],
            "triggers": [],
            "functions": [],
            "tables": []
        }
        
        try:
            # Drop views
            for view in ["tmodule_order_details", "tmodule_next_pending", "tmodule_approval_stats"]:
                cursor.execute(f"DROP VIEW IF EXISTS {view} CASCADE")
                dropped_items["views"].append(view)
            
            # Drop triggers
            triggers = [
                ("tmodule_orders_generate_number", "tmodule_orders"),
                ("tmodule_orders_update", "tmodule_orders"),
                ("tmodule_times_update", "tmodule_times"),
                ("tmodule_cases_update", "tmodule_cases"),
                ("tmodule_customers_update", "tmodule_customers")
            ]
            for trigger_name, table_name in triggers:
                cursor.execute(f"DROP TRIGGER IF EXISTS {trigger_name} ON {table_name}")
                dropped_items["triggers"].append(trigger_name)
            
            # Drop functions
            for func in ["tmodule_generate_order_number", "tmodule_update_timestamp"]:
                cursor.execute(f"DROP FUNCTION IF EXISTS {func}() CASCADE")
                dropped_items["functions"].append(func)
            
            # Drop tables
            for table in [
                "tmodule_sync_log",
                "tmodule_order_lines",
                "tmodule_orders",
                "tmodule_times",
                "tmodule_cases",
                "tmodule_customers",
                "tmodule_metadata"
            ]:
                cursor.execute(f"DROP TABLE IF EXISTS {table} CASCADE")
                dropped_items["tables"].append(table)
            
            conn.commit()
            
            logger.warning(f"✅ Module uninstalled - deleted {total_rows} rows")
            
            return TModuleUninstallResult(
                success=True,
                message=f"Time Tracking Module successfully uninstalled. Deleted {total_rows} rows.",
                tables_dropped=dropped_items["tables"],
                views_dropped=dropped_items["views"],
                functions_dropped=dropped_items["functions"],
                rows_deleted=total_rows
            )
        
        except Exception as e:
            conn.rollback()
            raise e
        finally:
            cursor.close()
            conn.close()
    
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"❌ Uninstall failed: {e}")
        raise HTTPException(status_code=500, detail=str(e))


# ============================================================================
# INTERNAL / HUB INTEGRATION ENDPOINTS
# ============================================================================

@router.get("/time", tags=["Internal"])
async def list_time_entries_v1(
    sag_id: int = Query(..., gt=0),
    day: Optional[date] = Query(None),
    medarbejder_id: Optional[int] = Query(None, gt=0),
):
    """List tidsregistreringer for en sag med filtre til timeline UI."""
    try:
        clauses = ["t.sag_id = %s"]
        params: List[Any] = [sag_id]

        if day:
            clauses.append("(t.worked_date = %s OR DATE(t.start_tid) = %s)")
            params.extend([day, day])

        if medarbejder_id:
            clauses.append("t.medarbejder_id = %s")
            params.append(medarbejder_id)

        where_sql = " AND ".join(clauses)
        query = f"""
            SELECT t.*, u.full_name AS employee_display_name, u.username AS employee_username
            FROM tmodule_times t
            LEFT JOIN users u ON u.user_id = t.medarbejder_id
            WHERE {where_sql}
            ORDER BY COALESCE(t.start_tid, t.worked_date::timestamp, t.created_at) DESC, t.id DESC
        """
        return execute_query(query, tuple(params))
    except Exception as e:
        logger.error("❌ Error listing v1 time entries for sag %s: %s", sag_id, e)
        raise HTTPException(status_code=500, detail="Failed to list time entries")


@router.post("/time/start", tags=["Internal"])
async def start_live_timer_v1(
    payload: Dict[str, Any] = Body(...),
    current_user: Optional[dict] = Depends(get_optional_user)
):
    """Start live timer. Kun én aktiv timer pr. bruger; eksisterende auto-pause'es."""
    try:
        sag_id = payload.get("sag_id")
        if not sag_id:
            raise HTTPException(status_code=400, detail="sag_id is required")

        customer_id = _resolve_case_customer_id(sag_id, payload.get("customer_id"))
        if customer_id is None:
            raise HTTPException(
                status_code=400,
                detail="Kunde er ikke linked til tidsmodulet. Kør kundesync/linking for kunden før tidsregistrering.",
            )

        bruger_id = _resolve_target_user_id(current_user, payload.get("medarbejder_id"))
        if not bruger_id:
            raise HTTPException(status_code=400, detail="medarbejder_id could not be resolved")

        now = datetime.now()
        existing = execute_query_single(
            """
            SELECT id, start_tid, round_block_min, pause_total_seconds, paused_at
            FROM tmodule_times
            WHERE medarbejder_id = %s AND aktiv_timer = TRUE AND slut_tid IS NULL
            ORDER BY start_tid DESC NULLS LAST, id DESC
            LIMIT 1
            """,
            (bruger_id,)
        )

        paused_entry = None
        if existing:
            actual_minutes = _elapsed_minutes_excluding_pause(existing, now)
            rounded_minutes = _round_up_minutes(actual_minutes, existing.get("round_block_min") or 30)
            pause_total_seconds = _pause_total_seconds_at(existing, now)
            execute_update(
                """
                UPDATE tmodule_times
                SET slut_tid = %s,
                    aktiv_timer = FALSE,
                    paused_at = NULL,
                    pause_total_seconds = %s,
                    faktisk_tid_min = %s,
                    fakturerbar_tid_min = CASE WHEN billable THEN %s ELSE 0 END,
                    original_hours = GREATEST(%s::numeric / 60.0, 0.01),
                    approved_hours = CASE WHEN billable THEN (%s::numeric / 60.0) ELSE NULL END,
                    rounded_to = CASE WHEN billable THEN (%s::numeric / 60.0) ELSE NULL END,
                    entry_status = 'afventer',
                    status = 'pending'
                WHERE id = %s
                """,
                (
                    now,
                    pause_total_seconds,
                    actual_minutes,
                    rounded_minutes,
                    actual_minutes,
                    rounded_minutes,
                    existing.get("round_block_min") or 30,
                    existing["id"],
                )
            )
            paused_entry = existing["id"]

        default_user_name = (
            (current_user or {}).get("username")
            or (current_user or {}).get("full_name")
            or "Hub User"
        )
        user_name = payload.get("user_name") or default_user_name
        entry_type = payload.get("entry_type") or "manuel"
        kilde = payload.get("kilde") or "manuel"
        billable = bool(payload.get("fakturerbar", True))
        round_block_min = int(payload.get("round_block_min") or 30)

        created = execute_query(
            """
            INSERT INTO tmodule_times (
                sag_id, customer_id, description, original_hours,
                worked_date, user_name, status, billable,
                start_tid, slut_tid, faktisk_tid_min, fakturerbar_tid_min,
                entry_type, kilde, entry_status, medarbejder_id,
                aktiv_timer, round_block_min, ikke_placeret
            ) VALUES (
                %s, %s, %s, %s,
                %s, %s, %s, %s,
                %s, %s, %s, %s,
                %s, %s, %s, %s,
                %s, %s, %s
            ) RETURNING *
            """,
            (
                sag_id,
                customer_id,
                payload.get("beskrivelse") or payload.get("description"),
                0.01,
                now.date(),
                user_name,
                "pending",
                billable,
                now,
                None,
                None,
                None,
                entry_type,
                kilde,
                "kladde",
                bruger_id,
                True,
                round_block_min,
                False,
            )
        )

        return {
            "entry": created[0] if created else None,
            "paused_entry_id": paused_entry,
        }
    except HTTPException:
        raise
    except Exception as e:
        logger.error("❌ Error starting live timer: %s", e)
        raise HTTPException(status_code=500, detail="Failed to start timer")


@router.post("/time/stop", tags=["Internal"])
async def stop_live_timer_v1(
    payload: Dict[str, Any] = Body(...),
    current_user: Optional[dict] = Depends(get_optional_user)
):
    """Stop aktiv timer for bruger eller specifik entry."""
    try:
        now = datetime.now()
        time_id = payload.get("time_id")
        bruger_id = _resolve_target_user_id(current_user, payload.get("medarbejder_id"))

        if time_id:
            entry = execute_query_single("SELECT * FROM tmodule_times WHERE id = %s", (time_id,))
        else:
            if not bruger_id:
                raise HTTPException(status_code=400, detail="medarbejder_id could not be resolved")
            entry = execute_query_single(
                """
                SELECT *
                FROM tmodule_times
                WHERE medarbejder_id = %s AND aktiv_timer = TRUE AND slut_tid IS NULL
                ORDER BY start_tid DESC NULLS LAST, id DESC
                LIMIT 1
                """,
                (bruger_id,)
            )

        if not entry:
            raise HTTPException(status_code=404, detail="No active timer found")

        actual_minutes = payload.get("faktisk_tid_min")
        if actual_minutes is None:
            actual_minutes = _elapsed_minutes_excluding_pause(entry, now)
        actual_minutes = max(0, int(actual_minutes or 0))
        pause_total_seconds = _pause_total_seconds_at(entry, now)

        block_minutes = int(payload.get("round_block_min") or entry.get("round_block_min") or 30)
        manual_billable = payload.get("fakturerbar_tid_min")
        billable_minutes = int(manual_billable) if manual_billable is not None else _round_up_minutes(actual_minutes, block_minutes)
        entry_status = payload.get("entry_status") or "afventer"
        legacy_status = _legacy_status_from_entry_status(entry_status)

        result = execute_query(
            """
            UPDATE tmodule_times
            SET slut_tid = %s,
                aktiv_timer = FALSE,
                paused_at = NULL,
                pause_total_seconds = %s,
                faktisk_tid_min = %s,
                fakturerbar_tid_min = CASE WHEN billable THEN %s ELSE 0 END,
                original_hours = GREATEST(%s::numeric / 60.0, 0.01),
                approved_hours = CASE WHEN billable THEN (%s::numeric / 60.0) ELSE NULL END,
                rounded_to = CASE WHEN billable THEN (%s::numeric / 60.0) ELSE NULL END,
                worked_date = COALESCE(worked_date, %s),
                entry_status = %s,
                status = %s,
                ikke_placeret = FALSE
            WHERE id = %s
            RETURNING *
            """,
            (
                now,
                pause_total_seconds,
                actual_minutes,
                billable_minutes,
                actual_minutes,
                billable_minutes,
                block_minutes,
                now.date(),
                entry_status,
                legacy_status,
                entry["id"],
            )
        )

        return result[0] if result else None
    except HTTPException:
        raise
    except Exception as e:
        logger.error("❌ Error stopping live timer: %s", e)
        raise HTTPException(status_code=500, detail="Failed to stop timer")


@router.post("/time/pause", tags=["Internal"])
async def pause_live_timer_v1(
    current_user: Optional[dict] = Depends(get_optional_user)
):
    """Pause current active timer for authenticated user without closing the entry."""
    try:
        bruger_id = _resolve_current_user_id(current_user)
        if not bruger_id:
            raise HTTPException(status_code=401, detail="Authentication required")

        now = datetime.now()
        entry = execute_query_single(
            """
            SELECT *
            FROM tmodule_times
            WHERE medarbejder_id = %s
              AND aktiv_timer = TRUE
              AND slut_tid IS NULL
            ORDER BY start_tid DESC NULLS LAST, id DESC
            LIMIT 1
            """,
            (bruger_id,),
        )

        if not entry:
            raise HTTPException(status_code=404, detail="No active timer found")

        if entry.get("paused_at"):
            raise HTTPException(status_code=409, detail="Timer is already paused")

        paused = execute_query(
            """
            UPDATE tmodule_times
            SET paused_at = %s,
                aktiv_timer = FALSE
            WHERE id = %s AND medarbejder_id = %s
            RETURNING *
            """,
            (now, entry["id"], bruger_id),
        )

        return paused[0] if paused else None
    except HTTPException:
        raise
    except Exception as e:
        logger.error("❌ Error pausing live timer: %s", e)
        raise HTTPException(status_code=500, detail="Failed to pause timer")


@router.post("/time/resume", tags=["Internal"])
async def resume_live_timer_v1(
    payload: Dict[str, Any] = Body(default={}),
    current_user: Optional[dict] = Depends(get_optional_user)
):
    """Resume paused timer for authenticated user on the same entry."""
    try:
        bruger_id = _resolve_current_user_id(current_user)
        if not bruger_id:
            raise HTTPException(status_code=401, detail="Authentication required")

        now = datetime.now()
        time_id = payload.get("time_id")

        active = execute_query_single(
            """
            SELECT id
            FROM tmodule_times
            WHERE medarbejder_id = %s
              AND aktiv_timer = TRUE
              AND slut_tid IS NULL
            ORDER BY start_tid DESC NULLS LAST, id DESC
            LIMIT 1
            """,
            (bruger_id,),
        )
        if active:
            raise HTTPException(status_code=409, detail="An active timer already exists")

        if time_id:
            entry = execute_query_single(
                """
                SELECT *
                FROM tmodule_times
                WHERE id = %s
                  AND medarbejder_id = %s
                  AND slut_tid IS NULL
                """,
                (time_id, bruger_id),
            )
        else:
            entry = execute_query_single(
                """
                SELECT *
                FROM tmodule_times
                WHERE medarbejder_id = %s
                  AND paused_at IS NOT NULL
                  AND aktiv_timer = FALSE
                  AND slut_tid IS NULL
                ORDER BY paused_at DESC, id DESC
                LIMIT 1
                """,
                (bruger_id,),
            )

        if not entry:
            raise HTTPException(status_code=404, detail="No paused timer found")

        if not entry.get("paused_at"):
            raise HTTPException(status_code=409, detail="Timer is not paused")

        updated_pause_total = _pause_total_seconds_at(entry, now)
        resumed = execute_query(
            """
            UPDATE tmodule_times
            SET pause_total_seconds = %s,
                paused_at = NULL,
                aktiv_timer = TRUE
            WHERE id = %s AND medarbejder_id = %s
            RETURNING *
            """,
            (updated_pause_total, entry["id"], bruger_id),
        )

        return resumed[0] if resumed else None
    except HTTPException:
        raise
    except Exception as e:
        logger.error("❌ Error resuming live timer: %s", e)
        raise HTTPException(status_code=500, detail="Failed to resume timer")


@router.get("/time/my-switchable", tags=["Internal"])
async def list_my_switchable_timers_v1(
    current_user: Optional[dict] = Depends(get_optional_user)
):
    """List authenticated user's currently active and paused timers for switch-case UI."""
    try:
        bruger_id = _resolve_current_user_id(current_user)
        if not bruger_id:
            raise HTTPException(status_code=401, detail="Authentication required")

        active = execute_query(
            """
            SELECT t.*, u.full_name AS employee_display_name, u.username AS employee_username
            FROM tmodule_times t
            LEFT JOIN users u ON u.user_id = t.medarbejder_id
            WHERE t.medarbejder_id = %s
              AND t.slut_tid IS NULL
              AND t.aktiv_timer = TRUE
              AND t.paused_at IS NULL
            ORDER BY t.start_tid DESC NULLS LAST, t.id DESC
            """,
            (bruger_id,),
        )

        paused = execute_query(
            """
            SELECT t.*, u.full_name AS employee_display_name, u.username AS employee_username
            FROM tmodule_times t
            LEFT JOIN users u ON u.user_id = t.medarbejder_id
            WHERE t.medarbejder_id = %s
              AND t.slut_tid IS NULL
              AND t.paused_at IS NOT NULL
              AND t.aktiv_timer = FALSE
            ORDER BY t.paused_at DESC, t.id DESC
            """,
            (bruger_id,),
        )

        return {
            "active": active or [],
            "paused": paused or [],
        }
    except HTTPException:
        raise
    except Exception as e:
        logger.error("❌ Error listing switchable timers: %s", e)
        raise HTTPException(status_code=500, detail="Failed to list switchable timers")


@router.post("/time/manual", tags=["Internal"])
async def create_manual_time_v1(
    payload: Dict[str, Any] = Body(...),
    current_user: Optional[dict] = Depends(get_optional_user)
):
    """Create manuel tidsregistrering med/uden start-slut."""
    try:
        sag_id = payload.get("sag_id")
        if not sag_id:
            raise HTTPException(status_code=400, detail="sag_id is required")

        customer_id = _resolve_case_customer_id(sag_id, payload.get("customer_id"))
        if customer_id is None:
            raise HTTPException(
                status_code=400,
                detail="Kunde er ikke linked til tidsmodulet. Kør kundesync/linking for kunden før tidsregistrering.",
            )

        bruger_id = _resolve_target_user_id(current_user, payload.get("medarbejder_id"))
        default_user_name = (
            (current_user or {}).get("username")
            or (current_user or {}).get("full_name")
            or "Hub User"
        )

        start_tid = _parse_iso_datetime(payload.get("start_tid"))
        slut_tid = _parse_iso_datetime(payload.get("slut_tid"))
        actual_minutes = payload.get("faktisk_tid_min")
        if actual_minutes is None:
            actual_minutes = _minutes_between(start_tid, slut_tid)
        if actual_minutes is None:
            original_hours = float(payload.get("original_hours") or 0)
            actual_minutes = int(round(original_hours * 60))

        if actual_minutes <= 0:
            raise HTTPException(status_code=400, detail="faktisk_tid_min or original_hours must be > 0")

        round_block_min = int(payload.get("round_block_min") or 30)
        billable = bool(payload.get("fakturerbar", True))
        billable_minutes = payload.get("fakturerbar_tid_min")
        if billable_minutes is None:
            billable_minutes = _round_up_minutes(actual_minutes, round_block_min)
        billable_minutes = int(billable_minutes)

        worked_date = payload.get("worked_date")
        if not worked_date:
            if start_tid:
                worked_date = start_tid.date()
            elif slut_tid:
                worked_date = slut_tid.date()

        not_placed = bool(payload.get("ikke_placeret", False)) or (not start_tid and not slut_tid)
        entry_status = payload.get("entry_status") or "afventer"
        legacy_status = _legacy_status_from_entry_status(entry_status)

        query = """
            INSERT INTO tmodule_times (
                sag_id, solution_id, customer_id, description,
                original_hours, worked_date, user_name,
                status, billable, billing_method,
                start_tid, slut_tid, faktisk_tid_min, fakturerbar_tid_min,
                entry_type, kilde, entry_status, medarbejder_id,
                aktiv_timer, round_block_min, ikke_placeret,
                approved_hours, rounded_to
            ) VALUES (
                %s, %s, %s, %s,
                %s, %s, %s,
                %s, %s, %s,
                %s, %s, %s, %s,
                %s, %s, %s, %s,
                %s, %s, %s,
                %s, %s
            ) RETURNING *
        """

        inserted = execute_query(
            query,
            (
                sag_id,
                payload.get("solution_id"),
                customer_id,
                payload.get("beskrivelse") or payload.get("description"),
                max(actual_minutes / 60.0, 0.01),
                worked_date,
                payload.get("user_name") or default_user_name,
                legacy_status,
                billable,
                payload.get("billing_method") or ("invoice" if billable else "internal"),
                start_tid,
                slut_tid,
                actual_minutes,
                billable_minutes,
                payload.get("entry_type") or "manuel",
                payload.get("kilde") or "manuel",
                entry_status,
                bruger_id,
                False,
                round_block_min,
                not_placed,
                (billable_minutes / 60.0) if billable else None,
                (round_block_min / 60.0) if billable else None,
            )
        )
        return inserted[0] if inserted else None
    except HTTPException:
        raise
    except Exception as e:
        logger.error("❌ Error creating manual time entry: %s", e)
        raise HTTPException(status_code=500, detail="Failed to create manual entry")


@router.patch("/time/{time_id}", tags=["Internal"])
async def patch_time_entry_v1(
    time_id: int,
    payload: Dict[str, Any] = Body(...)
):
    """Patch udvalgte felter på tidsentry. Faktisk tid ændres kun via start/slut."""
    try:
        existing = execute_query_single("SELECT * FROM tmodule_times WHERE id = %s", (time_id,))
        if not existing:
            raise HTTPException(status_code=404, detail="Time entry not found")

        updates: Dict[str, Any] = {}
        allowed_direct = [
            "description", "entry_type", "kilde", "entry_status", "billable", "worked_date",
            "fakturerbar_tid_min", "round_block_min", "ikke_placeret", "medarbejder_id"
        ]
        for key in allowed_direct:
            if key in payload:
                updates[key] = payload.get(key)

        start_tid = _parse_iso_datetime(payload.get("start_tid")) if "start_tid" in payload else existing.get("start_tid")
        slut_tid = _parse_iso_datetime(payload.get("slut_tid")) if "slut_tid" in payload else existing.get("slut_tid")
        if "start_tid" in payload:
            updates["start_tid"] = start_tid
        if "slut_tid" in payload:
            updates["slut_tid"] = slut_tid

        recalculated_minutes = _minutes_between(start_tid, slut_tid)
        if recalculated_minutes is not None:
            updates["faktisk_tid_min"] = recalculated_minutes
            updates["original_hours"] = max(recalculated_minutes / 60.0, 0.01)
            if "fakturerbar_tid_min" not in updates:
                block = int(updates.get("round_block_min") or existing.get("round_block_min") or 30)
                updates["fakturerbar_tid_min"] = _round_up_minutes(recalculated_minutes, block)

        if "entry_status" in updates:
            updates["status"] = _legacy_status_from_entry_status(updates["entry_status"])

        if "billable" in updates and not updates.get("billable"):
            updates["fakturerbar_tid_min"] = 0

        if "fakturerbar_tid_min" in updates:
            billable_minutes = int(updates.get("fakturerbar_tid_min") or 0)
            updates["approved_hours"] = billable_minutes / 60.0 if (updates.get("billable", existing.get("billable")) and billable_minutes > 0) else 0

        if not updates:
            return existing

        set_parts = []
        values: List[Any] = []
        for field, value in updates.items():
            set_parts.append(f"{field} = %s")
            values.append(value)
        values.append(time_id)

        query = f"UPDATE tmodule_times SET {', '.join(set_parts)} WHERE id = %s RETURNING *"
        updated = execute_query(query, tuple(values))
        return updated[0] if updated else None
    except HTTPException:
        raise
    except Exception as e:
        logger.error("❌ Error patching time entry %s: %s", time_id, e)
        raise HTTPException(status_code=500, detail="Failed to patch time entry")


@router.post("/time/{time_id}/approve", tags=["Internal"])
async def approve_time_entry_v1(
    time_id: int,
    payload: Dict[str, Any] = Body(default={}),
    current_user: Optional[dict] = Depends(get_optional_user)
):
    """Approve time entry for billing (kræver type, med admin-override for ukendt)."""
    try:
        entry = execute_query_single("SELECT * FROM tmodule_times WHERE id = %s", (time_id,))
        if not entry:
            raise HTTPException(status_code=404, detail="Time entry not found")

        entry_type = payload.get("entry_type") or entry.get("entry_type") or "ukendt"
        is_admin_approver = bool((current_user or {}).get("is_superadmin") or (current_user or {}).get("is_shadow_admin"))
        if entry_type == "ukendt":
            if not is_admin_approver:
                raise HTTPException(status_code=400, detail="entry_type is required before approval")
            logger.warning("⚠️ Admin approved time entry with ukendt type (time_id=%s)", time_id)

        billable = bool(payload.get("fakturerbar", entry.get("billable", True)))
        billed_minutes = payload.get("fakturerbar_tid_min")
        if billed_minutes is None:
            billed_minutes = entry.get("fakturerbar_tid_min")
        if billed_minutes is None:
            faktisk = int(entry.get("faktisk_tid_min") or 0)
            billed_minutes = _round_up_minutes(faktisk, int(entry.get("round_block_min") or 30))
        billed_minutes = int(billed_minutes)

        approved_by = _resolve_current_user_id(current_user)
        updated = execute_query(
            """
            UPDATE tmodule_times
            SET entry_type = %s,
                entry_status = 'godkendt',
                status = 'approved',
                billable = %s,
                fakturerbar_tid_min = CASE WHEN %s THEN %s ELSE 0 END,
                approved_hours = CASE WHEN %s THEN (%s::numeric / 60.0) ELSE 0 END,
                approved_at = %s,
                approved_by = %s,
                aktiv_timer = FALSE
            WHERE id = %s
            RETURNING *
            """,
            (
                entry_type,
                billable,
                billable,
                billed_minutes,
                billable,
                billed_minutes,
                datetime.now(),
                approved_by,
                time_id,
            )
        )
        return updated[0] if updated else None
    except HTTPException:
        raise
    except Exception as e:
        logger.error("❌ Error approving time entry %s: %s", time_id, e)
        raise HTTPException(status_code=500, detail="Failed to approve time entry")

@router.get("/entries/sag/{sag_id}", tags=["Internal"])
async def get_time_entries_for_sag(sag_id: int):
    """Get time entries linked to a Hub Sag (Case)."""
    try:
        query = """
            SELECT * FROM tmodule_times 
            WHERE sag_id = %s 
            ORDER BY worked_date DESC, created_at DESC
        """
        results = execute_query(query, (sag_id,))
        return results
    except Exception as e:
        logger.error(f"❌ Error fetching time entries for sag {sag_id}: {e}")
        raise HTTPException(status_code=500, detail="Failed to fetch time entries")

@router.post("/entries/internal", tags=["Internal"])
async def create_internal_time_entry(
    entry: Dict[str, Any] = Body(...),
    current_user: Optional[dict] = Depends(get_optional_user)
):
    """
    Create a time entry manually (Internal/Hub).
    Requires: sag_id, original_hours
    Optional: customer_id (auto-resolved from sag if missing), solution_id
    """
    try:
        sag_id = entry.get("sag_id")
        solution_id = entry.get("solution_id")
        customer_id = entry.get("customer_id")
        description = entry.get("description")
        hours = entry.get("original_hours")
        worked_date = entry.get("worked_date") or datetime.now().date()
        start_tid = _parse_iso_datetime(entry.get("start_tid"))
        slut_tid = _parse_iso_datetime(entry.get("slut_tid"))
        default_user_name = (
            (current_user or {}).get("username")
            or (current_user or {}).get("full_name")
            or "Hub User"
        )
        user_name = entry.get("user_name") or default_user_name
        medarbejder_id = _resolve_current_user_id(current_user) or entry.get("medarbejder_id")
        prepaid_card_id = entry.get("prepaid_card_id")
        fixed_price_agreement_id = entry.get("fixed_price_agreement_id")
        work_type = entry.get("work_type", "support")
        is_internal = entry.get("is_internal", False)
        entry_type = entry.get("entry_type", "manuel")
        kilde = entry.get("kilde", "manuel")
        entry_status = entry.get("entry_status", "afventer")
        round_block_min = int(entry.get("round_block_min") or 30)
        
        if not sag_id or not hours:
             raise HTTPException(status_code=400, detail="sag_id and original_hours required")
             
        hours_decimal = float(hours)
        actual_minutes = entry.get("faktisk_tid_min")
        if actual_minutes is None:
            actual_minutes = _minutes_between(start_tid, slut_tid)
        if actual_minutes is None:
            actual_minutes = int(round(hours_decimal * 60))

        billable_minutes = entry.get("fakturerbar_tid_min")
        if billable_minutes is None:
            billable_minutes = _round_up_minutes(actual_minutes, round_block_min)

        # Auto-resolve customer if missing
        if not customer_id:
            # Get Hub Customer ID from Sag (fallback to linked customers)
            sag = execute_query_single("SELECT customer_id FROM sag_sager WHERE id = %s", (sag_id,))
            if not sag:
                raise HTTPException(status_code=404, detail="Sag not found")
            hub_customer_id = sag.get("customer_id")

            if not hub_customer_id:
                linked_customer = execute_query_single(
                    "SELECT customer_id FROM sag_kunder WHERE sag_id = %s AND deleted_at IS NULL ORDER BY id ASC LIMIT 1",
                    (sag_id,)
                )
                hub_customer_id = linked_customer.get("customer_id") if linked_customer else None

            if hub_customer_id:
                # Find matching tmodule_customer
                tm_cust = execute_query_single("SELECT id FROM tmodule_customers WHERE hub_customer_id = %s", (hub_customer_id,))
                if tm_cust:
                    customer_id = tm_cust["id"]
                else:
                    raise HTTPException(
                        status_code=400,
                        detail=f"Customer (Hub ID: {hub_customer_id}) not found in Time Module. Please sync customers."
                    )
            else:
                raise HTTPException(status_code=400, detail="Sag has no customer linked")
                 
        # Handle Prepaid Card
        billing_method = entry.get('billing_method', 'invoice')
        status = 'pending'
        billable = True
        
        if is_internal:
            billing_method = 'internal'
            billable = False
        elif prepaid_card_id:
            # Verify card
            card = execute_query_single("SELECT * FROM tticket_prepaid_cards WHERE id = %s", (prepaid_card_id,))
            if not card:
                 raise HTTPException(status_code=404, detail="Prepaid card not found")

            rounding_minutes = int(card.get('rounding_minutes') or 0)
            rounded_hours = hours_decimal
            rounded_to = None
            if rounding_minutes > 0:
                from decimal import Decimal, ROUND_CEILING
                interval = Decimal(rounding_minutes) / Decimal(60)
                rounded_hours = float((Decimal(str(hours_decimal)) / interval).to_integral_value(rounding=ROUND_CEILING) * interval)
                rounded_to = float(interval)

            if float(card['remaining_hours']) < rounded_hours:
                 # Optional: Allow overdraft? For now, block.
                 raise HTTPException(status_code=400, detail=f"Insufficient hours on prepaid card (Remaining: {card['remaining_hours']})")
                 
            # Deduct hours (remaining_hours is generated; update used_hours instead)
            new_used = float(card['used_hours']) + rounded_hours
            execute_update(
                "UPDATE tticket_prepaid_cards SET used_hours = %s WHERE id = %s",
                (new_used, prepaid_card_id)
            )
            updated_card = execute_query_single(
                "SELECT remaining_hours FROM tticket_prepaid_cards WHERE id = %s",
                (prepaid_card_id,)
            )
            if updated_card and float(updated_card['remaining_hours']) <= 0:
                execute_update(
                    "UPDATE tticket_prepaid_cards SET status = 'depleted' WHERE id = %s",
                    (prepaid_card_id,)
                )
            if rounded_to:
                entry['approved_hours'] = rounded_hours
                entry['rounded_to'] = rounded_to
            logger.info(f"💳 Deducted {rounded_hours} hours from prepaid card {prepaid_card_id}")
            
            billing_method = 'prepaid'
            status = 'billed' # Mark as processed/billed so it skips invoicing
            
        elif fixed_price_agreement_id:
            # Verify agreement
            agreement = execute_query_single("""
                SELECT a.*, bp.id as period_id, bp.used_hours, bp.included_hours
                FROM customer_fixed_price_agreements a
                LEFT JOIN fixed_price_billing_periods bp ON (
                    a.id = bp.agreement_id
                    AND bp.period_start <= CURRENT_DATE
                    AND bp.period_end >= CURRENT_DATE
                )
                WHERE a.id = %s AND a.status = 'active'
            """, (fixed_price_agreement_id,))
            
            if not agreement:
                raise HTTPException(status_code=404, detail="Fixed-price agreement not found or inactive")
            
            if not agreement.get('period_id'):
                # Auto-create billing period for current month
                today = datetime.now().date()
                period_start = date(today.year, today.month, 1)
                last_day = monthrange(today.year, today.month)[1]
                period_end = date(today.year, today.month, last_day)
                
                # Full month amount (auto-created periods are always full months)
                base_amount = float(agreement['monthly_hours']) * float(agreement['hourly_rate'])
                included_hours = float(agreement['monthly_hours'])
                
                execute_query("""
                    INSERT INTO fixed_price_billing_periods (
                        agreement_id, period_start, period_end,
                        included_hours, base_amount, status
                    ) VALUES (%s, %s, %s, %s, %s, %s)
                    RETURNING *
                """, (
                    fixed_price_agreement_id,
                    period_start,
                    period_end,
                    included_hours,
                    base_amount,
                    'active'
                ))
                
                # Re-query to get the new period
                agreement = execute_query_single("""
                    SELECT a.*, bp.id as period_id, bp.used_hours, bp.included_hours
                    FROM customer_fixed_price_agreements a
                    LEFT JOIN fixed_price_billing_periods bp ON (
                        a.id = bp.agreement_id
                        AND bp.period_start <= CURRENT_DATE
                        AND bp.period_end >= CURRENT_DATE
                    )
                    WHERE a.id = %s AND a.status = 'active'
                """, (fixed_price_agreement_id,))
                
                logger.info(f"📅 Auto-created billing period for agreement {fixed_price_agreement_id}: {period_start} to {period_end}")
            
            # Apply rounding
            rounding_minutes = int(agreement.get('rounding_minutes') or 0)
            rounded_hours = hours_decimal
            rounded_to = None
            if rounding_minutes > 0:
                from decimal import Decimal, ROUND_CEILING
                interval = Decimal(rounding_minutes) / Decimal(60)
                rounded_hours = float((Decimal(str(hours_decimal)) / interval).to_integral_value(rounding=ROUND_CEILING) * interval)
                rounded_to = float(interval)
            
            # Update period used_hours
            new_used = float(agreement['used_hours'] or 0) + rounded_hours
            execute_update(
                "UPDATE fixed_price_billing_periods SET used_hours = %s WHERE id = %s",
                (new_used, agreement['period_id'])
            )
            
            # Check if overtime
            if new_used > float(agreement['included_hours']):
                overtime = new_used - float(agreement['included_hours'])
                logger.warning(f"⚠️ Fixed-price agreement {fixed_price_agreement_id} has {overtime:.2f}h overtime")
                # Update period status to pending_approval if not already
                execute_update("""
                    UPDATE fixed_price_billing_periods 
                    SET status = 'pending_approval'
                    WHERE id = %s AND status = 'active'
                """, (agreement['period_id'],))
            
            if rounded_to:
                entry['approved_hours'] = rounded_hours
                entry['rounded_to'] = rounded_to
            
            logger.info(f"📋 Logged {rounded_hours} hours to fixed-price agreement {fixed_price_agreement_id}")
            
            billing_method = 'fixed_price'
            status = 'billed'  # Tracked in agreement, not invoiced separately
            
        elif billing_method == 'internal' or billing_method == 'warranty':
            billable = False

        query = """
            INSERT INTO tmodule_times (
                sag_id, solution_id, customer_id, description, 
                original_hours, worked_date, user_name, 
                status, billable, billing_method, prepaid_card_id, fixed_price_agreement_id, work_type,
                approved_hours, rounded_to,
                start_tid, slut_tid, faktisk_tid_min, fakturerbar_tid_min,
                entry_type, kilde, entry_status, medarbejder_id,
                aktiv_timer, round_block_min, ikke_placeret
            ) VALUES (
                %s, %s, %s, %s, 
                %s, %s, %s, 
                %s, %s, %s, %s, %s, %s,
                %s, %s,
                %s, %s, %s, %s,
                %s, %s, %s, %s,
                %s, %s, %s
            ) RETURNING *
        """
        
        params = (
            sag_id, solution_id, customer_id, description, 
            hours, worked_date, user_name, 
            status, billable, billing_method, prepaid_card_id, fixed_price_agreement_id, work_type,
            entry.get('approved_hours'), entry.get('rounded_to'),
            start_tid, slut_tid, actual_minutes, billable_minutes,
            entry_type, kilde, entry_status, medarbejder_id,
            False, round_block_min, bool(entry.get("ikke_placeret", False) or (not start_tid and not slut_tid))
        )
        result = execute_query(query, params)
        if result:
            return result[0]
        raise HTTPException(status_code=500, detail="Failed to create entry")
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"❌ Error creating internal time entry: {e}")
        raise HTTPException(status_code=500, detail=str(e))


# ============================================================================
# SERVICE CONTRACT MIGRATION WIZARD ENDPOINTS
# ============================================================================

@router.get("/service-contracts", response_model=List[Dict[str, Any]], tags=["Service Contracts"])
async def list_service_contracts():
    """
    Fetch active service contracts from vTiger for wizard dropdown
    
    Returns:
        List of contracts: id, contract_number, subject, account_id, account_name
    """
    try:
        contracts = await ServiceContractWizardService.get_active_contracts()
        logger.info(f"✅ Retrieved {len(contracts)} service contracts")
        return contracts
        
    except Exception as e:
        logger.error(f"❌ Error fetching service contracts: {e}")
        raise HTTPException(status_code=500, detail=str(e))


@router.post("/service-contracts/wizard/load", response_model=ServiceContractWizardData, tags=["Service Contracts"])
async def load_contract_wizard_data(
    request_data: Dict[str, Any] = Body(...),
    dry_run: bool = Query(False)
):
    """
    Load contract data for wizard: cases + timelogs + available klippekort
    
    Args:
        contract_id: vTiger service contract ID (from body) - required
        account_id: vTiger account ID (from body) - optional, will be looked up if empty
        dry_run: Preview mode (no changes) (from query param)
        
    Returns:
        Contract data with items to process
    """
    try:
        contract_id = request_data.get('contract_id')
        account_id = request_data.get('account_id', '')
        
        if not contract_id:
            logger.error("❌ contract_id is required in request body")
            raise HTTPException(status_code=400, detail="contract_id is required")
        
        logger.info(f"📥 Loading contract data: {contract_id} (dry_run={dry_run})")
        
        contract_data = await ServiceContractWizardService.load_contract_detailed_data(
            contract_id, account_id
        )
        
        if not contract_data:
            raise HTTPException(status_code=404, detail="Service contract not found or no cases/timelogs")
        
        return ServiceContractWizardData(**contract_data)
        
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"❌ Error loading contract data: {e}", exc_info=True)
        raise HTTPException(status_code=500, detail=str(e))


@router.post("/service-contracts/wizard/archive-case", response_model=ServiceContractWizardAction, tags=["Service Contracts"])
async def archive_case_in_wizard(
    request_data: Dict[str, Any] = Body(...),
    dry_run: bool = Query(False)
):
    """
    Archive a single case to tticket_archived_tickets
    
    Args:
        case_id: vTiger case ID (from body)
        case_data: Complete case data from vTiger (from body)
        contract_id: Service contract ID (for reference) (from body)
        dry_run: Preview mode (from query param)
        
    Returns:
        Action result with success/failure
    """
    try:
        case_id = request_data.get('case_id')
        case_data = request_data.get('case_data', {})
        contract_id = request_data.get('contract_id', '')
        
        if not case_id or not case_data:
            raise HTTPException(status_code=400, detail="case_id and case_data are required")
        
        logger.info(f"🔄 Archiving case {case_id} (dry_run={dry_run})")
        
        success, message, archived_id = ServiceContractWizardService.archive_case(
            case_data, contract_id, dry_run=dry_run
        )
        
        action = ServiceContractWizardAction(
            type='archive',
            item_id=case_id,
            title=case_data.get('title', case_data.get('subject', 'Untitled')),
            success=success,
            message=message,
            dry_run=dry_run,
            result_id=archived_id
        )
        
        return action
        
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"❌ Error archiving case: {e}")
        case_id = request_data.get('case_id', 'unknown')
        action = ServiceContractWizardAction(
            type='archive',
            item_id=case_id,
            title="Unknown",
            success=False,
            message=f"Error: {str(e)}",
            dry_run=dry_run,
            result_id=None
        )
        return action


@router.post("/service-contracts/wizard/transfer-timelog", response_model=ServiceContractWizardAction, tags=["Service Contracts"])
async def transfer_timelog_in_wizard(
    request: TimologTransferRequest
):
    """
    Transfer timelog hours to customer's klippekort
    
    Args:
        request: TimologTransferRequest with timelog, card, customer IDs + dry_run flag
        
    Returns:
        Action result with success/failure
    """
    try:
        logger.info(f"🔄 Transferring timelog {request.timelog_id} to card {request.card_id} (dry_run={request.dry_run})")
        
        # Fetch complete timelog data from vTiger
        vtiger_svc = get_vtiger_service()
        timelog = await vtiger_svc.query(f"SELECT * FROM Timelog WHERE id='{request.timelog_id}';")
        
        if not timelog:
            raise HTTPException(status_code=404, detail=f"Timelog {request.timelog_id} not found")
        
        timelog_data = timelog[0]
        
        success, message, transaction = ServiceContractWizardService.transfer_timelog_to_klippekort(
            timelog_data,
            request.card_id,
            request.customer_id,
            request.contract_id,
            dry_run=request.dry_run
        )
        
        action = ServiceContractWizardAction(
            type='transfer',
            item_id=request.timelog_id,
            title=f"{timelog_data.get('hours', 0)}h - {timelog_data.get('description', '')}",
            success=success,
            message=message,
            dry_run=request.dry_run,
            result_id=transaction.get('id') if transaction else None
        )
        
        return action
        
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"❌ Error transferring timelog: {e}")
        action = ServiceContractWizardAction(
            type='transfer',
            item_id=request.timelog_id,
            title="Unknown",
            success=False,
            message=f"Error: {str(e)}",
            dry_run=request.dry_run,
            result_id=None
        )
        return action


@router.get("/service-contracts/wizard/customer-cards/{customer_id}", response_model=List[Dict[str, Any]], tags=["Service Contracts"])
async def get_customer_klippekort_cards(customer_id: int):
    """
    Get active klippekort cards for a customer (for wizard dropdown)
    
    Args:
        customer_id: Hub customer ID
        
    Returns:
        List of active prepaid cards
    """
    try:
        cards = KlippekortService.get_active_cards_for_customer(customer_id)
        logger.info(f"✅ Retrieved {len(cards)} active cards for customer {customer_id}")
        return cards
        
    except Exception as e:
        logger.error(f"❌ Error fetching customer cards: {e}")
        raise HTTPException(status_code=500, detail=str(e))


# ============================================================================
# SERVICE CONTRACT REPORT ENDPOINTS
# ============================================================================

def _build_service_contract_report_pdf(report_data: ServiceContractReportData) -> bytes:
    """Generate PDF bytes from report payload for customer delivery."""
    try:
        from reportlab.lib.pagesizes import A4
        from reportlab.lib.units import mm
        from reportlab.pdfgen import canvas
    except ModuleNotFoundError as exc:
        raise RuntimeError("PDF library not installed: reportlab") from exc

    buffer = BytesIO()
    pdf = canvas.Canvas(buffer, pagesize=A4)
    page_width, page_height = A4

    left = 18 * mm
    right = page_width - (18 * mm)
    y = page_height - (20 * mm)

    def ensure_space(lines_needed: int = 1):
        nonlocal y
        min_y = 20 * mm
        required = lines_needed * 6 * mm
        if y - required < min_y:
            pdf.showPage()
            y = page_height - (20 * mm)

    def draw_line(text: str, size: int = 10, bold: bool = False):
        nonlocal y
        ensure_space(1)
        font_name = "Helvetica-Bold" if bold else "Helvetica"
        pdf.setFont(font_name, size)
        pdf.drawString(left, y, text[:140])
        y -= 6 * mm

    pdf.setTitle("Servicekontrakt Rapport")
    draw_line("BMC Hub - Servicekontrakt Rapport", size=14, bold=True)
    draw_line(f"Kunde: {report_data.customer.account_name} ({report_data.customer.account_id})", bold=True)
    draw_line(
        f"Kontrakt: {report_data.contract.contract_number or '-'} | {report_data.contract.subject or '-'}",
        bold=False,
    )
    if report_data.contract.vtiger_url:
        draw_line(f"vTiger: {report_data.contract.vtiger_url}", size=9)
    draw_line(f"Genereret: {report_data.generated_at.strftime('%Y-%m-%d %H:%M')}", bold=False)
    y -= 2 * mm

    summary = report_data.summary
    draw_line(
        (
            f"Total cases: {summary.total_cases} | Timelogs: {summary.total_timelogs} | Timer: {summary.total_hours}"
        ),
        bold=True,
    )
    y -= 2 * mm

    for case_item in report_data.cases:
        draw_line(
            f"Case {case_item.cc_number or case_item.id}: {case_item.title}",
            size=11,
            bold=True,
        )
        if case_item.vtiger_url:
            draw_line(f"Case link: {case_item.vtiger_url}", size=8)
        draw_line(f"Kontaktperson: {case_item.contact_person or '-'}", size=9)
        draw_line(f"Beskrivelse: {case_item.description or '-'}", size=9)
        draw_line(
            (
                f"Prioritet: {case_item.priority or '-'} | "
                f"Timelogs: {case_item.timelog_count} | Timer: {case_item.total_hours}"
            ),
            size=9,
        )
        if not case_item.timelogs:
            draw_line("Ingen timelogs fundet", size=9)
            y -= 1 * mm
            continue

        for timelog in case_item.timelogs:
            description = (timelog.description or "").replace("\n", " ").strip()
            if len(description) > 70:
                description = f"{description[:67]}..."
            draw_line(
                (
                    f"- {timelog.worked_date or '-'} | {timelog.employee_initials or '-'} | "
                    f"{timelog.hours}h | {description}"
                ),
                size=8,
            )
            if timelog.vtiger_url:
                draw_line(f"  Link: {timelog.vtiger_url}", size=7)
        y -= 2 * mm

    pdf.showPage()
    pdf.save()
    buffer.seek(0)
    return buffer.getvalue()


def _build_service_contract_report_excel(report_data: ServiceContractReportData) -> bytes:
    """Generate Excel bytes from report payload for customer delivery."""
    try:
        from openpyxl import Workbook
        from openpyxl.styles import Font
    except ModuleNotFoundError as exc:
        raise RuntimeError("Excel library not installed: openpyxl") from exc

    workbook = Workbook()
    sheet = workbook.active
    sheet.title = "Servicekontrakt"

    sheet.append(["Kunde", report_data.customer.account_name])
    sheet.append(["Kunde ID", report_data.customer.account_id])
    sheet.append(["Kontrakt", report_data.contract.contract_number or "-"])
    sheet.append(["Kontrakt titel", report_data.contract.subject or "-"])
    sheet.append(["Kontrakt link", report_data.contract.vtiger_url or ""])
    sheet.append(["Genereret", report_data.generated_at.strftime("%Y-%m-%d %H:%M")])
    sheet.append([])

    headers = [
        "CC nummer",
        "Case ID",
        "Case titel",
        "Case link",
        "Kontaktperson",
        "Case beskrivelse",
        "Prioritet",
        "Timelog ID",
        "Timelog link",
        "Dato",
        "Medarbejder initialer",
        "Timer",
        "Log beskrivelse",
    ]
    sheet.append(headers)
    header_row = sheet.max_row
    for col_idx in range(1, len(headers) + 1):
        sheet.cell(row=header_row, column=col_idx).font = Font(bold=True)

    for case_item in report_data.cases:
        timelogs = case_item.timelogs or []
        if not timelogs:
            sheet.append([
                case_item.cc_number or case_item.id,
                case_item.id,
                case_item.title,
                case_item.vtiger_url or "",
                case_item.contact_person or "",
                case_item.description or "",
                case_item.priority or "",
                "",
                "",
                "",
                "",
                float(case_item.total_hours),
                "",
            ])
            continue

        for timelog in timelogs:
            sheet.append([
                case_item.cc_number or case_item.id,
                case_item.id,
                case_item.title,
                case_item.vtiger_url or "",
                case_item.contact_person or "",
                case_item.description or "",
                case_item.priority or "",
                timelog.id,
                timelog.vtiger_url or "",
                timelog.worked_date or "",
                timelog.employee_initials or "",
                float(timelog.hours),
                timelog.description or "",
            ])

    for col in ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M"]:
        sheet.column_dimensions[col].width = 22

    output = BytesIO()
    workbook.save(output)
    output.seek(0)
    return output.getvalue()


@router.get(
    "/service-contract-report/customers",
    response_model=List[ServiceContractReportCustomer],
    tags=["Service Contracts"],
)
async def get_service_contract_report_customers():
    """List customers with service contracts for report filter."""
    try:
        customers = await get_vtiger_service().get_service_contract_customers()
        return [ServiceContractReportCustomer(**item) for item in customers]
    except Exception as e:
        logger.error(f"❌ Error loading report customers: {e}")
        raise HTTPException(status_code=500, detail="Kunne ikke hente kunder til rapport")


@router.get(
    "/service-contract-report/contracts",
    response_model=List[ServiceContractReportContract],
    tags=["Service Contracts"],
)
async def get_service_contract_report_contracts(account_id: str = Query(..., min_length=2)):
    """List service contracts for a selected customer account."""
    try:
        vtiger = get_vtiger_service()
        contracts = await vtiger.get_service_contracts(account_id=account_id, active_only=False)
        return [
            ServiceContractReportContract(
                id=contract.get("id", ""),
                contract_number=contract.get("contract_number") or contract.get("contract_no") or "",
                subject=contract.get("subject") or "",
                contract_status=contract.get("contract_status"),
            )
            for contract in contracts
            if contract.get("id")
        ]
    except Exception as e:
        logger.error(f"❌ Error loading contracts for report: {e}")
        raise HTTPException(status_code=500, detail="Kunne ikke hente servicekontrakter")


@router.get(
    "/service-contract-report/data",
    response_model=ServiceContractReportData,
    tags=["Service Contracts"],
)
async def get_service_contract_report_data(
    account_id: str = Query(..., min_length=2),
    contract_id: str = Query(..., min_length=2),
):
    """Get full case + timelog report for selected customer + contract."""
    try:
        payload = await get_vtiger_service().get_service_contract_report_data(account_id, contract_id)
        return ServiceContractReportData(**payload)
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
    except Exception as e:
        logger.error(f"❌ Error loading service contract report data: {e}")
        raise HTTPException(status_code=500, detail="Kunne ikke hente rapportdata fra vTiger")


@router.get("/service-contract-report/pdf", tags=["Service Contracts"])
async def download_service_contract_report_pdf(
    account_id: str = Query(..., min_length=2),
    contract_id: str = Query(..., min_length=2),
):
    """Generate and return PDF for selected customer + service contract."""
    try:
        payload = await get_vtiger_service().get_service_contract_report_data(account_id, contract_id)
        report_data = ServiceContractReportData(**payload)
        pdf_bytes = _build_service_contract_report_pdf(report_data)

        file_name = f"servicekontrakt-rapport-{report_data.contract.id}.pdf"
        headers = {
            "Content-Disposition": f'attachment; filename="{file_name}"'
        }
        return Response(content=pdf_bytes, media_type="application/pdf", headers=headers)
    except RuntimeError as e:
        logger.error(f"❌ PDF dependency missing: {e}")
        raise HTTPException(status_code=503, detail="PDF-funktion er ikke tilgaengelig endnu (mangler reportlab i container)")
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
    except Exception as e:
        logger.error(f"❌ Error generating service contract report PDF: {e}")
        raise HTTPException(status_code=500, detail="Kunne ikke generere PDF")


@router.get("/service-contract-report/excel", tags=["Service Contracts"])
async def download_service_contract_report_excel(
    account_id: str = Query(..., min_length=2),
    contract_id: str = Query(..., min_length=2),
):
    """Generate and return Excel for selected customer + service contract."""
    try:
        payload = await get_vtiger_service().get_service_contract_report_data(account_id, contract_id)
        report_data = ServiceContractReportData(**payload)
        excel_bytes = _build_service_contract_report_excel(report_data)

        file_name = f"servicekontrakt-rapport-{report_data.contract.id}.xlsx"
        headers = {
            "Content-Disposition": f'attachment; filename="{file_name}"'
        }
        media_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
        return Response(content=excel_bytes, media_type=media_type, headers=headers)
    except RuntimeError as e:
        logger.error(f"❌ Excel dependency missing: {e}")
        raise HTTPException(status_code=503, detail="Excel-funktion er ikke tilgaengelig endnu (mangler openpyxl i container)")
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
    except Exception as e:
        logger.error(f"❌ Error generating service contract report Excel: {e}")
        raise HTTPException(status_code=500, detail="Kunne ikke generere Excel")
