import asyncio
import json
import time

from fastapi import APIRouter, HTTPException
from fastapi.responses import HTMLResponse, StreamingResponse

from app.models import GenericJSONResponse

debug_router = APIRouter(
    prefix="/api/1rx/v1/scheduler/debug",
    tags=["Debug / Inspection"],
)


def _state():
    """Lazy import to avoid circular dependency with main."""
    from app import main

    return {
        "redis": main.redis,
        "vendor_keys": main.vendor_keys,
        "doctors_data": main.doctors_data,
        "VENDOR_DOCTOR_MAPPING": main.VENDOR_DOCTOR_MAPPING,
        "ASSIGNED_DOCTORS_KEY": main.ASSIGNED_DOCTORS_KEY,
        "PRESCRIPTION_QUEUE": main.PRESCRIPTION_QUEUE,
        "PRESCRIPTION_VENDOR_MAP_KEY": main.PRESCRIPTION_VENDOR_MAP_KEY,
        "RX_ASSIGNMENT_MAPPING_KEY": main.RX_ASSIGNMENT_MAPPING_KEY,
        "RX_PRESCRIPTION_MAPPING_KEY": main.RX_PRESCRIPTION_MAPPING_KEY,
    }


@debug_router.get("/rx_assignment_mapping/{prescription_id}")
async def get_rx_assignment_mapping(prescription_id: str):
    """Get assignment mapping details for a specific prescription."""
    s = _state()
    data = await s["redis"].hget(s["RX_ASSIGNMENT_MAPPING_KEY"], prescription_id)
    if not data:
        raise HTTPException(
            status_code=404,
            detail=f"No assignment mapping found for prescription {prescription_id}.",
        )
    return GenericJSONResponse(data=json.loads(data))


@debug_router.get("/rx_assignment_mapping")
async def get_all_rx_assignment_mappings():
    """Get all assignment mappings (assignment_time, vendor_id, is_blocked)."""
    s = _state()
    raw = await s["redis"].hgetall(s["RX_ASSIGNMENT_MAPPING_KEY"])
    if not raw:
        return GenericJSONResponse(data={})
    result = {}
    for prescription_id, value in raw.items():
        result[prescription_id] = json.loads(value)
    return GenericJSONResponse(data=result)


@debug_router.get("/rx_prescription_mapping/{prescription_id}")
async def get_rx_prescription_mapping(prescription_id: str):
    """Get prescription mapping (create_time, vendor_id, tried_doctors) for a prescription."""
    s = _state()
    data = await s["redis"].hget(s["RX_PRESCRIPTION_MAPPING_KEY"], prescription_id)
    if not data:
        raise HTTPException(
            status_code=404,
            detail=f"No prescription mapping found for {prescription_id}.",
        )
    return GenericJSONResponse(data=json.loads(data))


@debug_router.get("/rx_prescription_mapping")
async def get_all_rx_prescription_mappings():
    """Get all prescription mappings."""
    s = _state()
    raw = await s["redis"].hgetall(s["RX_PRESCRIPTION_MAPPING_KEY"])
    if not raw:
        return GenericJSONResponse(data={})
    result = {}
    for prescription_id, value in raw.items():
        result[prescription_id] = json.loads(value)
    return GenericJSONResponse(data=result)


@debug_router.get("/vendor_cleanup/{vendor_id}")
async def vendor_cleanup_check(vendor_id: str):
    """
    Check for inconsistencies in a vendor's Redis state:
    - Doctors in pool that are also in assigned_doctors
    - Duplicate doctors in pool
    - Orphaned assignments (prescription assigned but no mapping)
    """
    s = _state()
    redis = s["redis"]
    vendor_keys = s["vendor_keys"]
    doctors_data = s["doctors_data"]

    redis_key = vendor_keys.get(vendor_id)
    if not redis_key:
        raise HTTPException(status_code=404, detail=f"Vendor {vendor_id} not found.")

    pool = await redis.lrange(redis_key, 0, -1)
    pool = [str(d) for d in pool]

    assigned_raw = await redis.hgetall(s["ASSIGNED_DOCTORS_KEY"])
    assignment_mappings = await redis.hgetall(s["RX_ASSIGNMENT_MAPPING_KEY"])

    issues = []

    # Check for duplicate doctors in pool
    seen = set()
    duplicates = []
    for doc in pool:
        if doc in seen:
            duplicates.append(doc)
        seen.add(doc)
    if duplicates:
        issues.append(
            {
                "type": "duplicate_in_pool",
                "message": f"Duplicate doctors in pool: {duplicates}",
                "doctors": duplicates,
            }
        )

    # Check for doctors in pool AND assigned
    for prescription_id, doctor_str in assigned_raw.items():
        mapping_data = assignment_mappings.get(prescription_id)
        if mapping_data:
            mapping = json.loads(mapping_data)
            if mapping.get("vendor_id") != vendor_id:
                continue

        doctor_ids = doctor_str.split(",")
        in_both = [d for d in doctor_ids if d in pool]
        if in_both:
            issues.append(
                {
                    "type": "assigned_and_in_pool",
                    "message": f"Doctors {in_both} are assigned to {prescription_id} but also in pool",
                    "prescription_id": prescription_id,
                    "doctors": in_both,
                }
            )

    # Check for orphaned assignments (in assigned_doctors but no assignment mapping)
    for prescription_id, doctor_str in assigned_raw.items():
        mapping_data = assignment_mappings.get(prescription_id)
        if mapping_data:
            mapping = json.loads(mapping_data)
            if mapping.get("vendor_id") != vendor_id:
                continue
        else:
            doctor_ids = doctor_str.split(",")
            vendor_doctors = []
            if doctors_data and len(doctors_data.data) > 0:
                vendor_doctors = next(
                    (
                        entry.doctors
                        for entry in doctors_data.data
                        if entry.vendor == vendor_id
                    ),
                    [],
                )
            if any(d in vendor_doctors for d in doctor_ids):
                issues.append(
                    {
                        "type": "orphaned_assignment",
                        "message": f"Prescription {prescription_id} has assigned doctors but no assignment mapping",
                        "prescription_id": prescription_id,
                        "doctors": doctor_ids,
                    }
                )

    return GenericJSONResponse(
        data={
            "vendor_id": vendor_id,
            "pool_size": len(pool),
            "pool_unique_size": len(seen),
            "pool_doctors": pool,
            "issues_count": len(issues),
            "issues": issues,
        }
    )


@debug_router.get("/state")
async def get_full_debug_state():
    """
    Single endpoint showing full Redis state:
    all vendor pools, all assignments, all queues, all mappings.
    """
    s = _state()
    redis = s["redis"]
    vendor_keys = s["vendor_keys"]
    state = {}

    # Vendor pools
    vendor_pools = {}
    for vid, rkey in vendor_keys.items():
        pool = await redis.lrange(rkey, 0, -1)
        vendor_pools[vid] = [str(d) for d in pool]
    state["vendor_pools"] = vendor_pools

    # Assigned doctors
    assigned_raw = await redis.hgetall(s["ASSIGNED_DOCTORS_KEY"])
    state["assigned_doctors"] = {
        pid: val.split(",") for pid, val in assigned_raw.items()
    }

    # Assignment mappings
    assignment_mappings_raw = await redis.hgetall(s["RX_ASSIGNMENT_MAPPING_KEY"])
    state["assignment_mappings"] = {
        pid: json.loads(val) for pid, val in assignment_mappings_raw.items()
    }

    # Prescription mappings
    prescription_mappings_raw = await redis.hgetall(s["RX_PRESCRIPTION_MAPPING_KEY"])
    state["prescription_mappings"] = {
        pid: json.loads(val) for pid, val in prescription_mappings_raw.items()
    }

    # Prescription queue
    queue = await redis.lrange(s["PRESCRIPTION_QUEUE"], 0, -1)
    state["prescription_queue"] = [str(p) for p in queue]

    # Vendor prescription map
    vendor_map_raw = await redis.hgetall(s["PRESCRIPTION_VENDOR_MAP_KEY"])
    state["vendor_prescription_map"] = {
        vid: json.loads(val) for vid, val in vendor_map_raw.items()
    }

    # Vendor config
    state["vendor_config"] = s["VENDOR_DOCTOR_MAPPING"]

    return GenericJSONResponse(data=state)


@debug_router.get("/live/stream")
async def live_state_stream(interval: int = 5):
    """SSE data stream endpoint — used by the dashboard."""

    async def event_generator():
        while True:
            try:
                s = _state()
                redis = s["redis"]
                vendor_keys = s["vendor_keys"]
                state = {}

                vendor_pools = {}
                for vid, rkey in vendor_keys.items():
                    pool = await redis.lrange(rkey, 0, -1)
                    vendor_pools[vid] = [str(d) for d in pool]
                state["vendor_pools"] = vendor_pools

                assigned_raw = await redis.hgetall(s["ASSIGNED_DOCTORS_KEY"])
                state["assigned_doctors"] = {
                    pid: val.split(",") for pid, val in assigned_raw.items()
                }

                assignment_mappings_raw = await redis.hgetall(
                    s["RX_ASSIGNMENT_MAPPING_KEY"]
                )
                state["assignment_mappings"] = {
                    pid: json.loads(val)
                    for pid, val in assignment_mappings_raw.items()
                }

                prescription_mappings_raw = await redis.hgetall(
                    s["RX_PRESCRIPTION_MAPPING_KEY"]
                )
                state["prescription_mappings"] = {
                    pid: json.loads(val)
                    for pid, val in prescription_mappings_raw.items()
                }

                queue = await redis.lrange(s["PRESCRIPTION_QUEUE"], 0, -1)
                state["prescription_queue"] = [str(p) for p in queue]

                vendor_map_raw = await redis.hgetall(s["PRESCRIPTION_VENDOR_MAP_KEY"])
                state["vendor_prescription_map"] = {
                    vid: json.loads(val) for vid, val in vendor_map_raw.items()
                }

                state["vendor_config"] = s["VENDOR_DOCTOR_MAPPING"]
                state["timestamp"] = int(time.time())

                yield f"data: {json.dumps(state)}\n\n"
            except Exception as e:
                yield f"data: {json.dumps({'error': str(e)})}\n\n"

            await asyncio.sleep(interval)

    return StreamingResponse(
        event_generator(),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
            "X-Accel-Buffering": "no",
        },
    )


@debug_router.get("/live", response_class=HTMLResponse)
async def live_dashboard(interval: int = 5):
    """
    Live HTML dashboard showing all Redis cache state in tables.
    Auto-refreshes via SSE. Open in browser:
    /api/1rx/v1/scheduler/debug/live?interval=5
    """
    html = f"""<!DOCTYPE html>
<html>
<head>
<title>Scheduler - Redis Live Dashboard</title>
<style>
  * {{ margin: 0; padding: 0; box-sizing: border-box; }}
  body {{ font-family: 'Segoe UI', Tahoma, sans-serif; background: #1a1a2e; color: #e0e0e0; padding: 20px; }}
  h1 {{ color: #00d4ff; margin-bottom: 5px; }}
  .header {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; border-bottom: 1px solid #333; padding-bottom: 10px; }}
  .status {{ font-size: 14px; color: #888; }}
  .status .live {{ color: #00ff88; font-weight: bold; }}
  .status .error {{ color: #ff4444; font-weight: bold; }}
  .grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px; }}
  .full-width {{ grid-column: 1 / -1; }}
  .card {{ background: #16213e; border: 1px solid #0f3460; border-radius: 8px; padding: 15px; }}
  .card h2 {{ color: #00d4ff; font-size: 16px; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center; }}
  .card h2 .count {{ background: #0f3460; color: #00d4ff; padding: 2px 8px; border-radius: 10px; font-size: 12px; }}
  table {{ width: 100%; border-collapse: collapse; font-size: 13px; }}
  th {{ background: #0f3460; color: #00d4ff; padding: 8px 10px; text-align: left; font-weight: 600; }}
  td {{ padding: 6px 10px; border-bottom: 1px solid #1a1a3e; word-break: break-all; }}
  tr:hover td {{ background: #1a2a4e; }}
  .tag {{ display: inline-block; background: #0f3460; color: #7ec8e3; padding: 2px 6px; border-radius: 3px; margin: 1px; font-size: 11px; }}
  .tag.blocked {{ background: #5c1a1a; color: #ff6b6b; }}
  .tag.active {{ background: #1a3a1a; color: #6bff6b; }}
  .empty {{ color: #555; font-style: italic; padding: 15px; text-align: center; }}
  .queue-item {{ display: inline-block; background: #2a1a4e; color: #c4a0ff; padding: 3px 8px; border-radius: 3px; margin: 2px; font-size: 12px; }}
</style>
</head>
<body>
<div class="header">
  <h1>Scheduler Redis Dashboard</h1>
  <div class="status">Status: <span id="conn-status" class="live">Connecting...</span> | Updated: <span id="last-update">-</span></div>
</div>
<div id="content"><div class="empty">Connecting to live stream...</div></div>

<script>
const STREAM_URL = '/api/1rx/v1/scheduler/debug/live/stream?interval={interval}';
let es;

function shortId(id) {{
  if (!id || id.length < 12) return id;
  return id.substring(0, 8) + '...';
}}

function formatTime(ts) {{
  if (!ts) return '-';
  const d = new Date(Number(ts) * 1000);
  return d.toLocaleTimeString();
}}

function renderDoctorTags(doctors) {{
  if (!doctors || doctors.length === 0) return '<span class="empty">none</span>';
  return doctors.map(d => '<span class="tag">' + shortId(d) + '</span>').join(' ');
}}

function render(state) {{
  if (state.error) {{
    document.getElementById('content').innerHTML = '<div class="empty">Error: ' + state.error + '</div>';
    return;
  }}

  let html = '<div class="grid">';

  // 1. Vendor Doctor Pools
  html += '<div class="card">';
  html += '<h2>Doctor Pools <span class="count">' + Object.keys(state.vendor_pools || {{}}).length + ' vendors</span></h2>';
  html += '<table><tr><th>Vendor</th><th>Count</th><th>Doctors</th></tr>';
  for (const [vid, docs] of Object.entries(state.vendor_pools || {{}})) {{
    html += '<tr><td><strong>' + vid + '</strong></td><td>' + docs.length + '</td><td>' + renderDoctorTags(docs) + '</td></tr>';
  }}
  html += '</table></div>';

  // 2. Prescription Queue
  const queue = state.prescription_queue || [];
  html += '<div class="card">';
  html += '<h2>Prescription Queue <span class="count">' + queue.length + '</span></h2>';
  if (queue.length === 0) {{
    html += '<div class="empty">Queue is empty</div>';
  }} else {{
    html += '<div style="padding:5px">';
    queue.forEach(p => {{ html += '<span class="queue-item">' + shortId(p) + '</span>'; }});
    html += '</div>';
  }}
  html += '</div>';

  // 2b. Vendor Prescription Map (per-vendor waiting prescriptions)
  const vendorMap = state.vendor_prescription_map || {{}};
  html += '<div class="card">';
  html += '<h2>Vendor Prescription Map <span class="count">' + Object.keys(vendorMap).length + ' vendors</span></h2>';
  if (Object.keys(vendorMap).length === 0) {{
    html += '<div class="empty">No waiting prescriptions</div>';
  }} else {{
    html += '<table><tr><th>Vendor</th><th>Count</th><th>Prescriptions</th></tr>';
    for (const [vid, rxList] of Object.entries(vendorMap)) {{
      html += '<tr><td><strong>' + vid + '</strong></td><td>' + rxList.length + '</td><td>';
      rxList.forEach(p => {{ html += '<span class="queue-item">' + shortId(p) + '</span>'; }});
      html += '</td></tr>';
    }}
    html += '</table>';
  }}
  html += '</div>';

  // 3. Assigned Doctors (full width)
  const assigned = state.assigned_doctors || {{}};
  const assignmentMappings = state.assignment_mappings || {{}};
  html += '<div class="card full-width">';
  html += '<h2>Assigned Doctors <span class="count">' + Object.keys(assigned).length + ' prescriptions</span></h2>';
  if (Object.keys(assigned).length === 0) {{
    html += '<div class="empty">No active assignments</div>';
  }} else {{
    html += '<table><tr><th>Prescription ID</th><th>Doctors</th><th>Vendor</th><th>Blocked</th><th>Assigned At</th></tr>';
    for (const [pid, docs] of Object.entries(assigned)) {{
      const mapping = assignmentMappings[pid] || {{}};
      const blocked = mapping.is_blocked ? '<span class="tag blocked">BLOCKED</span>' : '<span class="tag active">ACTIVE</span>';
      const vendor = mapping.vendor_id || '-';
      const assignedAt = formatTime(mapping.assignment_time);
      html += '<tr><td>' + shortId(pid) + '</td><td>' + renderDoctorTags(docs) + '</td><td>' + vendor + '</td><td>' + blocked + '</td><td>' + assignedAt + '</td></tr>';
    }}
    html += '</table>';
  }}
  html += '</div>';

  // 4. Prescription Mappings (full width)
  const pMappings = state.prescription_mappings || {{}};
  html += '<div class="card full-width">';
  html += '<h2>Prescription Mappings (Queue Details) <span class="count">' + Object.keys(pMappings).length + '</span></h2>';
  if (Object.keys(pMappings).length === 0) {{
    html += '<div class="empty">No prescription mappings</div>';
  }} else {{
    html += '<table><tr><th>Prescription ID</th><th>Vendor</th><th>Create Time</th><th>Tried Doctors</th></tr>';
    for (const [pid, data] of Object.entries(pMappings)) {{
      const tried = data.tried_doctors || [];
      html += '<tr><td>' + shortId(pid) + '</td><td>' + (data.vendor_id || '-') + '</td><td>' + formatTime(data.prescription_create_time) + '</td><td>' + (tried.length > 0 ? renderDoctorTags(tried) : '<span class="empty">none</span>') + '</td></tr>';
    }}
    html += '</table>';
  }}
  html += '</div>';

  // 5. Vendor Config
  const config = state.vendor_config || {{}};
  html += '<div class="card full-width">';
  html += '<h2>Vendor Configuration <span class="count">' + Object.keys(config).length + ' vendors</span></h2>';
  if (Object.keys(config).length === 0) {{
    html += '<div class="empty">No vendor config loaded</div>';
  }} else {{
    html += '<table><tr><th>Vendor</th><th>Concurrent Doctors</th><th>Max Hold</th><th>Max Block</th><th>Notification Interval</th><th>Delay After Hold</th><th>Immediate Unblock</th></tr>';
    for (const [vid, cfg] of Object.entries(config)) {{
      html += '<tr><td><strong>' + vid + '</strong></td>';
      html += '<td>' + (cfg.concurrent_doctors === null ? 'ALL' : cfg.concurrent_doctors) + '</td>';
      html += '<td>' + (cfg.rx_max_hold_time || '-') + 's</td>';
      html += '<td>' + (cfg.rx_max_block_time || '-') + 's</td>';
      html += '<td>' + (cfg.rx_notification_interval || '-') + 's</td>';
      html += '<td>' + (cfg.delay_after_hold || 0) + 's</td>';
      html += '<td>' + (cfg.immediate_unblock ? 'Yes' : 'No') + '</td></tr>';
    }}
    html += '</table>';
  }}
  html += '</div>';

  html += '</div>';
  document.getElementById('content').innerHTML = html;
}}

function connect() {{
  es = new EventSource(STREAM_URL);
  document.getElementById('conn-status').className = 'live';
  document.getElementById('conn-status').textContent = 'Connected';

  es.onmessage = function(event) {{
    try {{
      const state = JSON.parse(event.data);
      render(state);
      document.getElementById('last-update').textContent = new Date().toLocaleTimeString();
      document.getElementById('conn-status').className = 'live';
      document.getElementById('conn-status').textContent = 'Live';
    }} catch(e) {{
      console.error('Parse error:', e);
    }}
  }};

  es.onerror = function() {{
    document.getElementById('conn-status').className = 'error';
    document.getElementById('conn-status').textContent = 'Disconnected - Retrying...';
    es.close();
    setTimeout(connect, 3000);
  }};
}}

connect();
</script>
</body>
</html>"""
    return HTMLResponse(content=html)
