from fastapi import FastAPI, APIRouter, HTTPException, Query
from fastapi.concurrency import run_in_threadpool
from dotenv import load_dotenv
from starlette.middleware.cors import CORSMiddleware
from motor.motor_asyncio import AsyncIOMotorClient
import os
import logging
import random
from pathlib import Path
from pydantic import BaseModel, Field, field_validator
from typing import List, Optional
import uuid
from datetime import datetime, timezone, timedelta, date, time as dtime
from zoneinfo import ZoneInfo

ROOT_DIR = Path(__file__).parent
load_dotenv(ROOT_DIR / '.env')

mongo_url = os.environ['MONGO_URL']
client = AsyncIOMotorClient(mongo_url)
db = client[os.environ['DB_NAME']]

IST = ZoneInfo("Asia/Kolkata")
ORG_ID = "org-minovis-demo"
ORG_NAME = "Minovis Demo Hospital"

app = FastAPI(title="Minovis HMS")
api_router = APIRouter(prefix="/api")

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)


# ----------------------------- Helpers -----------------------------

def now_utc() -> datetime:
    return datetime.now(timezone.utc)


def to_ist(dt: datetime) -> datetime:
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=timezone.utc)
    return dt.astimezone(IST)


def iso(dt: datetime) -> str:
    return dt.astimezone(timezone.utc).isoformat()


def parse_dt(s: str) -> datetime:
    dt = datetime.fromisoformat(s)
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=timezone.utc)
    return dt


def ist_today() -> date:
    return now_utc().astimezone(IST).date()


def ist_day_bounds(d: date):
    start_ist = datetime.combine(d, dtime.min, tzinfo=IST)
    end_ist = start_ist + timedelta(days=1)
    return start_ist.astimezone(timezone.utc), end_ist.astimezone(timezone.utc)


def gen_uuid() -> str:
    return str(uuid.uuid4())


async def next_counter(name: str) -> int:
    doc = await db.counters.find_one_and_update(
        {"_id": name}, {"$inc": {"seq": 1}}, upsert=True, return_document=True
    )
    return doc["seq"]


def clean(doc: dict) -> dict:
    if doc and "_id" in doc:
        doc = {k: v for k, v in doc.items() if k != "_id"}
    return doc


def los(admission_time: str, end: Optional[datetime] = None) -> dict:
    start = parse_dt(admission_time)
    end = end or now_utc()
    delta = end - start
    total_hours = int(delta.total_seconds() // 3600)
    days = total_hours // 24
    hours = total_hours % 24
    return {"days": days, "hours": hours, "label": f"{days}d {hours}h"}


# ----------------------------- Models -----------------------------

class PatientCreate(BaseModel):
    full_name: str
    dob: str
    gender: Optional[str] = None
    mobile: str
    email: Optional[str] = None
    city: Optional[str] = None
    emergency_contact_name: Optional[str] = None
    emergency_contact_phone: Optional[str] = None
    confirm_duplicate: bool = False

    @field_validator("full_name")
    @classmethod
    def name_required(cls, v):
        if not v or not v.strip():
            raise ValueError("Full name is required")
        return v.strip()

    @field_validator("dob")
    @classmethod
    def dob_valid(cls, v):
        d = datetime.fromisoformat(v).date()
        if d > ist_today():
            raise ValueError("Date of birth cannot be in the future")
        return v

    @field_validator("mobile")
    @classmethod
    def mobile_valid(cls, v):
        digits = "".join(ch for ch in v if ch.isdigit())
        if digits.startswith("91") and len(digits) == 12:
            digits = digits[2:]
        if len(digits) != 10 or digits[0] not in "6789":
            raise ValueError("Enter a valid 10-digit Indian mobile number")
        return digits


class PatientUpdate(BaseModel):
    full_name: Optional[str] = None
    dob: Optional[str] = None
    gender: Optional[str] = None
    mobile: Optional[str] = None
    email: Optional[str] = None
    city: Optional[str] = None
    emergency_contact_name: Optional[str] = None
    emergency_contact_phone: Optional[str] = None


class OPDCreate(BaseModel):
    patient_id: str
    doctor_id: str
    department: str
    visit_mode: str  # walk-in | appointment
    scheduled_time: Optional[str] = None
    reason: Optional[str] = None


class AdmissionCreate(BaseModel):
    patient_id: str
    doctor_id: str
    department: str
    admission_time: str
    reason: Optional[str] = None
    ward_id: str
    bed_id: str


class TransferRequest(BaseModel):
    bed_id: str


class DischargeRequest(BaseModel):
    discharge_time: str
    note: Optional[str] = None


# ----------------------------- Activity -----------------------------

async def add_activity(kind: str, description: str, refs: dict = None):
    evt = {
        "id": gen_uuid(),
        "org_id": ORG_ID,
        "kind": kind,
        "description": description,
        "refs": refs or {},
        "timestamp": iso(now_utc()),
    }
    await db.activity.insert_one(evt)


def age_from_dob(dob: str) -> int:
    d = datetime.fromisoformat(dob).date()
    t = ist_today()
    return t.year - d.year - ((t.month, t.day) < (d.month, d.day))


def initials(name: str) -> str:
    parts = [p for p in name.split() if p]
    if not parts:
        return "?"
    if len(parts) == 1:
        return parts[0][:2].upper()
    return (parts[0][0] + parts[-1][0]).upper()


# ----------------------------- Patient serialization -----------------------------

def serialize_patient(p: dict) -> dict:
    p = clean(p)
    p["age"] = age_from_dob(p["dob"])
    p["initials"] = initials(p["full_name"])
    return p


# ----------------------------- Patients -----------------------------

@api_router.get("/patients")
async def list_patients(search: str = "", page: int = 1, limit: int = 10):
    q = {"org_id": ORG_ID}
    if search:
        s = search.strip()
        q["$or"] = [
            {"full_name": {"$regex": s, "$options": "i"}},
            {"patient_id": {"$regex": s, "$options": "i"}},
            {"mobile": {"$regex": s, "$options": "i"}},
        ]
    total = await db.patients.count_documents(q)
    skip = (page - 1) * limit
    docs = await db.patients.find(q, {"_id": 0}).sort("created_at", -1).skip(skip).limit(limit).to_list(limit)
    return {
        "total": total,
        "page": page,
        "limit": limit,
        "patients": [serialize_patient(d) for d in docs],
    }


@api_router.get("/patients/check-phone")
async def check_phone(mobile: str):
    digits = "".join(ch for ch in mobile if ch.isdigit())
    if digits.startswith("91") and len(digits) == 12:
        digits = digits[2:]
    docs = await db.patients.find({"org_id": ORG_ID, "mobile": digits}, {"_id": 0}).to_list(10)
    return {"matches": [serialize_patient(d) for d in docs]}


@api_router.post("/patients")
async def create_patient(data: PatientCreate):
    if not data.confirm_duplicate:
        existing = await db.patients.find_one({"org_id": ORG_ID, "mobile": data.mobile})
        if existing:
            raise HTTPException(status_code=409, detail="A patient with this mobile number already exists. Confirm to continue with a shared family number.")
    seq = await next_counter("patient")
    pid = f"MNV-{seq:06d}"
    doc = {
        "id": gen_uuid(),
        "org_id": ORG_ID,
        "patient_id": pid,
        "full_name": data.full_name,
        "dob": data.dob,
        "gender": data.gender,
        "mobile": data.mobile,
        "email": data.email,
        "city": data.city,
        "emergency_contact_name": data.emergency_contact_name,
        "emergency_contact_phone": data.emergency_contact_phone,
        "created_at": iso(now_utc()),
        "last_encounter_at": None,
        "synthetic": True,
    }
    await db.patients.insert_one(doc)
    await add_activity("registration", f"Registered patient {data.full_name} ({pid})", {"patient_id": doc["id"]})
    return serialize_patient(doc)


@api_router.get("/patients/{pid}")
async def get_patient(pid: str):
    p = await db.patients.find_one({"id": pid, "org_id": ORG_ID})
    if not p:
        raise HTTPException(404, "Patient not found")
    return serialize_patient(p)


@api_router.put("/patients/{pid}")
async def update_patient(pid: str, data: PatientUpdate):
    p = await db.patients.find_one({"id": pid, "org_id": ORG_ID})
    if not p:
        raise HTTPException(404, "Patient not found")
    update = {k: v for k, v in data.model_dump().items() if v is not None}
    if "dob" in update:
        d = datetime.fromisoformat(update["dob"]).date()
        if d > ist_today():
            raise HTTPException(422, "Date of birth cannot be in the future")
    if "mobile" in update:
        digits = "".join(ch for ch in update["mobile"] if ch.isdigit())
        if digits.startswith("91") and len(digits) == 12:
            digits = digits[2:]
        if len(digits) != 10 or digits[0] not in "6789":
            raise HTTPException(422, "Enter a valid 10-digit Indian mobile number")
        update["mobile"] = digits
    await db.patients.update_one({"id": pid}, {"$set": update})
    p = await db.patients.find_one({"id": pid})
    await add_activity("patient_update", f"Updated patient {p['full_name']} ({p['patient_id']})", {"patient_id": pid})
    return serialize_patient(p)


async def enrich_opd(e: dict) -> dict:
    e = clean(e)
    p = await db.patients.find_one({"id": e["patient_id"]}, {"_id": 0})
    doc = await db.doctors.find_one({"id": e["doctor_id"]}, {"_id": 0})
    e["patient"] = serialize_patient(p) if p else None
    e["doctor_name"] = doc["name"] if doc else "—"
    return e


async def enrich_admission(a: dict) -> dict:
    a = clean(a)
    p = await db.patients.find_one({"id": a["patient_id"]}, {"_id": 0})
    doc = await db.doctors.find_one({"id": a["doctor_id"]}, {"_id": 0})
    bed = await db.beds.find_one({"id": a["bed_id"]}, {"_id": 0})
    a["patient"] = serialize_patient(p) if p else None
    a["doctor_name"] = doc["name"] if doc else "—"
    a["bed"] = clean(bed) if bed else None
    end = parse_dt(a["discharge_time"]) if a.get("discharge_time") else None
    a["los"] = los(a["admission_time"], end)
    return a


@api_router.get("/patients/{pid}/profile")
async def patient_profile(pid: str):
    p = await db.patients.find_one({"id": pid, "org_id": ORG_ID})
    if not p:
        raise HTTPException(404, "Patient not found")
    opd = await db.opd.find({"patient_id": pid}, {"_id": 0}).sort("created_at", -1).to_list(200)
    adm = await db.admissions.find({"patient_id": pid}, {"_id": 0}).sort("admission_time", -1).to_list(200)
    acts = await db.activity.find({"refs.patient_id": pid}, {"_id": 0}).sort("timestamp", -1).to_list(100)
    opd_e = [await enrich_opd(e) for e in opd]
    adm_e = [await enrich_admission(a) for a in adm]
    current = next((a for a in adm_e if a["status"] == "Active"), None)
    return {
        "patient": serialize_patient(p),
        "opd_history": opd_e,
        "admissions": adm_e,
        "current_admission": current,
        "activity": acts,
    }


# ----------------------------- Doctors / Departments / Wards -----------------------------

@api_router.get("/doctors")
async def list_doctors():
    docs = await db.doctors.find({"org_id": ORG_ID}, {"_id": 0}).to_list(100)
    return docs


@api_router.get("/departments")
async def list_departments():
    docs = await db.doctors.find({"org_id": ORG_ID}, {"_id": 0}).to_list(100)
    return sorted(list({d["department"] for d in docs}))


@api_router.get("/wards")
async def list_wards():
    docs = await db.wards.find({"org_id": ORG_ID}, {"_id": 0}).to_list(100)
    return docs


@api_router.get("/doctors/{doc_id}/slots")
async def doctor_slots(doc_id: str, date: str):
    doc = await db.doctors.find_one({"id": doc_id}, {"_id": 0})
    if not doc:
        raise HTTPException(404, "Doctor not found")
    d = datetime.fromisoformat(date).date()
    slots = []
    start = datetime.combine(d, dtime(9, 0), tzinfo=IST)
    end = datetime.combine(d, dtime(17, 0), tzinfo=IST)
    # existing active appointments for that doctor/day
    day_start_utc, day_end_utc = ist_day_bounds(d)
    existing = await db.opd.find({
        "doctor_id": doc_id,
        "visit_mode": "appointment",
        "status": {"$in": ["Scheduled", "Waiting", "In consultation"]},
        "scheduled_time": {"$gte": iso(day_start_utc), "$lt": iso(day_end_utc)},
    }, {"_id": 0}).to_list(200)
    taken = {parse_dt(e["scheduled_time"]).astimezone(IST).strftime("%H:%M") for e in existing}
    cur = start
    now = now_utc()
    while cur < end:
        label = cur.strftime("%H:%M")
        is_past = cur.astimezone(timezone.utc) < now
        slots.append({
            "time": iso(cur),
            "label": cur.strftime("%I:%M %p"),
            "available": (label not in taken) and (not is_past),
        })
        cur += timedelta(minutes=20)
    return slots


# ----------------------------- OPD -----------------------------

ACTIVE_OPD = ["Scheduled", "Waiting", "In consultation"]


@api_router.get("/opd/summary")
async def opd_summary(date: Optional[str] = None):
    d = datetime.fromisoformat(date).date() if date else ist_today()
    start, end = ist_day_bounds(d)
    q = {"org_id": ORG_ID, "day": d.isoformat()}
    docs = await db.opd.find(q, {"_id": 0}).to_list(500)
    counts = {"scheduled": 0, "waiting": 0, "in_consultation": 0, "completed": 0}
    for e in docs:
        if e["status"] == "Scheduled":
            counts["scheduled"] += 1
        elif e["status"] == "Waiting":
            counts["waiting"] += 1
        elif e["status"] == "In consultation":
            counts["in_consultation"] += 1
        elif e["status"] == "Completed":
            counts["completed"] += 1
    return counts


@api_router.get("/opd")
async def list_opd(date: Optional[str] = None, doctor: str = "", department: str = "",
                   status: str = "", search: str = "", view: str = "queue"):
    q = {"org_id": ORG_ID}
    if date:
        q["day"] = date
    if doctor:
        q["doctor_id"] = doctor
    if department:
        q["department"] = department
    if status:
        q["status"] = status
    docs = await db.opd.find(q, {"_id": 0}).sort("created_at", 1).to_list(500)
    enriched = [await enrich_opd(e) for e in docs]
    if search:
        s = search.lower()
        enriched = [e for e in enriched if e["patient"] and (s in e["patient"]["full_name"].lower() or s in e["patient"]["patient_id"].lower())]
    if view == "queue":
        enriched = [e for e in enriched if e["status"] in ACTIVE_OPD]
        enriched.sort(key=lambda e: e.get("token", ""))
    return enriched


@api_router.get("/opd/{eid}")
async def get_opd(eid: str):
    e = await db.opd.find_one({"id": eid}, {"_id": 0})
    if not e:
        raise HTTPException(404, "OPD visit not found")
    return await enrich_opd(e)


@api_router.post("/opd")
async def create_opd(data: OPDCreate):
    p = await db.patients.find_one({"id": data.patient_id, "org_id": ORG_ID})
    if not p:
        raise HTTPException(404, "Patient not found")
    doc = await db.doctors.find_one({"id": data.doctor_id})
    if not doc:
        raise HTTPException(404, "Doctor not found")

    if data.visit_mode == "appointment":
        if not data.scheduled_time:
            raise HTTPException(422, "Appointment time is required")
        sched = parse_dt(data.scheduled_time)
        if sched < now_utc():
            raise HTTPException(422, "Appointment time cannot be in the past")
        slot_label = sched.astimezone(IST).strftime("%H:%M")
        day = sched.astimezone(IST).date()
        day_start_utc, day_end_utc = ist_day_bounds(day)
        conflict = await db.opd.find_one({
            "doctor_id": data.doctor_id,
            "visit_mode": "appointment",
            "status": {"$in": ACTIVE_OPD},
            "scheduled_time": {"$gte": iso(day_start_utc), "$lt": iso(day_end_utc)},
        })
        # check same slot
        existing = await db.opd.find({
            "doctor_id": data.doctor_id,
            "visit_mode": "appointment",
            "status": {"$in": ACTIVE_OPD},
            "scheduled_time": {"$gte": iso(day_start_utc), "$lt": iso(day_end_utc)},
        }, {"_id": 0}).to_list(200)
        for ex in existing:
            if parse_dt(ex["scheduled_time"]).astimezone(IST).strftime("%H:%M") == slot_label:
                raise HTTPException(409, "This time slot is no longer available for the selected doctor.")
        day_iso = day.isoformat()
        status = "Scheduled"
        checkin_time = None
    else:
        sched = None
        day = ist_today()
        day_iso = day.isoformat()
        status = "Waiting"
        checkin_time = iso(now_utc())

    seq = await next_counter(f"opd-token-{day_iso}")
    token = f"OPD-{seq:03d}"
    ts = iso(now_utc())
    history = [{"status": status, "at": ts}]
    e = {
        "id": gen_uuid(),
        "org_id": ORG_ID,
        "token": token,
        "day": day_iso,
        "patient_id": data.patient_id,
        "doctor_id": data.doctor_id,
        "department": data.department,
        "visit_mode": data.visit_mode,
        "scheduled_time": iso(sched) if sched else None,
        "checkin_time": checkin_time,
        "reason": data.reason,
        "status": status,
        "status_history": history,
        "created_at": ts,
    }
    await db.opd.insert_one(e)
    await db.patients.update_one({"id": data.patient_id}, {"$set": {"last_encounter_at": ts}})
    await add_activity("opd_checkin", f"OPD visit {token} for {p['full_name']} with {doc['name']}", {"patient_id": data.patient_id, "opd_id": e["id"]})
    return await enrich_opd(e)


async def transition_opd(eid: str, new_status: str, allowed_from: List[str]):
    e = await db.opd.find_one({"id": eid})
    if not e:
        raise HTTPException(404, "OPD visit not found")
    if e["status"] not in allowed_from:
        raise HTTPException(409, f"Cannot move visit from {e['status']} to {new_status}")
    ts = iso(now_utc())
    update = {"status": new_status}
    if new_status == "Waiting" and not e.get("checkin_time"):
        update["checkin_time"] = ts
    await db.opd.update_one({"id": eid}, {"$set": update, "$push": {"status_history": {"status": new_status, "at": ts}}})
    e = await db.opd.find_one({"id": eid})
    return await enrich_opd(e)


@api_router.post("/opd/{eid}/checkin")
async def opd_checkin(eid: str):
    return await transition_opd(eid, "Waiting", ["Scheduled"])


@api_router.post("/opd/{eid}/start")
async def opd_start(eid: str):
    return await transition_opd(eid, "In consultation", ["Waiting"])


@api_router.post("/opd/{eid}/complete")
async def opd_complete(eid: str):
    res = await transition_opd(eid, "Completed", ["In consultation", "Waiting"])
    await add_activity("opd_complete", f"Completed OPD visit {res['token']}", {"patient_id": res["patient_id"], "opd_id": eid})
    return res


@api_router.post("/opd/{eid}/cancel")
async def opd_cancel(eid: str):
    res = await transition_opd(eid, "Cancelled", ["Scheduled", "Waiting"])
    await add_activity("opd_cancel", f"Cancelled OPD visit {res['token']}", {"patient_id": res["patient_id"], "opd_id": eid})
    return res


# ----------------------------- Beds -----------------------------

@api_router.get("/beds/summary")
async def beds_summary():
    beds = await db.beds.find({"org_id": ORG_ID}, {"_id": 0}).to_list(200)
    summary = {"total": len(beds), "Available": 0, "Occupied": 0, "Cleaning": 0, "Maintenance": 0}
    for b in beds:
        summary[b["status"]] = summary.get(b["status"], 0) + 1
    return summary


async def enrich_bed(b: dict) -> dict:
    b = clean(b)
    if b["status"] == "Occupied":
        adm = await db.admissions.find_one({"bed_id": b["id"], "status": "Active"}, {"_id": 0})
        if adm:
            p = await db.patients.find_one({"id": adm["patient_id"]}, {"_id": 0})
            doc = await db.doctors.find_one({"id": adm["doctor_id"]}, {"_id": 0})
            b["admission"] = {
                "id": adm["id"],
                "admission_id": adm["admission_id"],
                "patient_name": p["full_name"] if p else "—",
                "patient_code": p["patient_id"] if p else "—",
                "doctor_name": doc["name"] if doc else "—",
                "admission_time": adm["admission_time"],
            }
    return b


@api_router.get("/beds")
async def list_beds(floor: str = "", ward: str = "", bed_type: str = "", status: str = "", search: str = ""):
    q = {"org_id": ORG_ID}
    if floor:
        q["floor"] = floor
    if ward:
        q["ward_id"] = ward
    if bed_type:
        q["bed_type"] = bed_type
    if status:
        q["status"] = status
    beds = await db.beds.find(q, {"_id": 0}).to_list(200)
    enriched = [await enrich_bed(b) for b in beds]
    if search:
        s = search.lower()
        enriched = [b for b in enriched if s in b["bed_number"].lower() or (b.get("admission") and s in b["admission"]["patient_name"].lower())]
    # order by floor then bed number
    enriched.sort(key=lambda b: (b["floor_order"], b["bed_number"]))
    return enriched


@api_router.get("/beds/{bid}")
async def get_bed(bid: str):
    b = await db.beds.find_one({"id": bid}, {"_id": 0})
    if not b:
        raise HTTPException(404, "Bed not found")
    enriched = await enrich_bed(b)
    hist = await db.bed_history.find({"bed_id": bid}, {"_id": 0}).sort("at", -1).to_list(50)
    enriched["history"] = hist
    return enriched


async def bed_status_change(bid: str, from_status: str, to_status: str, note: str):
    updated = await db.beds.find_one_and_update(
        {"id": bid, "status": from_status},
        {"$set": {"status": to_status}},
    )
    if not updated:
        cur = await db.beds.find_one({"id": bid}, {"_id": 0})
        raise HTTPException(409, f"Bed is currently {cur['status'] if cur else 'unknown'} and cannot be moved to {to_status}.")
    ts = iso(now_utc())
    await db.bed_history.insert_one({"id": gen_uuid(), "bed_id": bid, "event": note, "from": from_status, "to": to_status, "at": ts})
    await add_activity("bed_readiness", f"Bed {updated['bed_number']} {note.lower()}", {"bed_id": bid})
    b = await db.beds.find_one({"id": bid}, {"_id": 0})
    return await enrich_bed(b)


@api_router.post("/beds/{bid}/maintenance")
async def bed_to_maintenance(bid: str):
    return await bed_status_change(bid, "Available", "Maintenance", "Moved to Maintenance")


@api_router.post("/beds/{bid}/mark-available")
async def bed_mark_available(bid: str):
    return await bed_status_change(bid, "Cleaning", "Available", "Marked Available")


@api_router.post("/beds/{bid}/return-to-cleaning")
async def bed_return_cleaning(bid: str):
    return await bed_status_change(bid, "Maintenance", "Cleaning", "Returned to service (Cleaning)")


# ----------------------------- Admissions (IPD) -----------------------------

@api_router.get("/admissions/summary")
async def admissions_summary():
    today = ist_today()
    start, end = ist_day_bounds(today)
    active = await db.admissions.count_documents({"org_id": ORG_ID, "status": "Active"})
    adm_today = await db.admissions.count_documents({"org_id": ORG_ID, "admission_time": {"$gte": iso(start), "$lt": iso(end)}})
    disc_today = await db.admissions.count_documents({"org_id": ORG_ID, "discharge_time": {"$gte": iso(start), "$lt": iso(end)}})
    return {"currently_admitted": active, "admissions_today": adm_today, "discharges_today": disc_today}


@api_router.get("/admissions")
async def list_admissions(status: str = "Active", search: str = "", doctor: str = "",
                          ward: str = "", date_from: str = "", date_to: str = ""):
    q = {"org_id": ORG_ID}
    if status:
        q["status"] = status
    if doctor:
        q["doctor_id"] = doctor
    if ward:
        q["ward_id"] = ward
    if date_from:
        q.setdefault("admission_time", {})["$gte"] = iso(datetime.combine(datetime.fromisoformat(date_from).date(), dtime.min, tzinfo=IST).astimezone(timezone.utc))
    if date_to:
        q.setdefault("admission_time", {})["$lt"] = iso((datetime.combine(datetime.fromisoformat(date_to).date(), dtime.min, tzinfo=IST) + timedelta(days=1)).astimezone(timezone.utc))
    docs = await db.admissions.find(q, {"_id": 0}).sort("admission_time", -1).to_list(500)
    enriched = [await enrich_admission(a) for a in docs]
    if search:
        s = search.lower()
        enriched = [a for a in enriched if a["patient"] and (s in a["patient"]["full_name"].lower() or s in a["patient"]["patient_id"].lower())]
    return enriched


@api_router.get("/admissions/{aid}")
async def get_admission(aid: str):
    a = await db.admissions.find_one({"id": aid}, {"_id": 0})
    if not a:
        raise HTTPException(404, "Admission not found")
    return await enrich_admission(a)


@api_router.post("/admissions")
async def create_admission(data: AdmissionCreate):
    p = await db.patients.find_one({"id": data.patient_id, "org_id": ORG_ID})
    if not p:
        raise HTTPException(404, "Patient not found")
    doc = await db.doctors.find_one({"id": data.doctor_id})
    if not doc:
        raise HTTPException(404, "Doctor not found")
    adm_dt = parse_dt(data.admission_time)
    if adm_dt > now_utc():
        raise HTTPException(422, "Admission time cannot be in the future")
    existing_active = await db.admissions.find_one({"patient_id": data.patient_id, "status": "Active"})
    if existing_active:
        raise HTTPException(409, "This patient already has an active admission.")

    # atomic bed claim: only Available beds
    bed = await db.beds.find_one_and_update(
        {"id": data.bed_id, "org_id": ORG_ID, "status": "Available", "ward_id": data.ward_id},
        {"$set": {"status": "Occupied"}},
    )
    if not bed:
        cur = await db.beds.find_one({"id": data.bed_id}, {"_id": 0})
        raise HTTPException(409, f"Selected bed is no longer available (currently {cur['status'] if cur else 'unknown'}). Please choose another bed.")

    seq = await next_counter("admission")
    aid_code = f"IPD-{seq:06d}"
    ts = iso(now_utc())
    assign = {"bed_id": data.bed_id, "bed_number": bed["bed_number"], "ward_id": data.ward_id, "ward_name": bed["ward_name"], "from": data.admission_time, "to": None}
    a = {
        "id": gen_uuid(),
        "org_id": ORG_ID,
        "admission_id": aid_code,
        "patient_id": data.patient_id,
        "doctor_id": data.doctor_id,
        "department": data.department,
        "ward_id": data.ward_id,
        "bed_id": data.bed_id,
        "admission_time": data.admission_time,
        "discharge_time": None,
        "reason": data.reason,
        "status": "Active",
        "bed_assignments": [assign],
        "discharge_note": None,
        "created_at": ts,
    }
    await db.admissions.insert_one(a)
    await db.bed_history.insert_one({"id": gen_uuid(), "bed_id": data.bed_id, "event": f"Assigned to {p['full_name']} ({aid_code})", "from": "Available", "to": "Occupied", "at": ts})
    await db.patients.update_one({"id": data.patient_id}, {"$set": {"last_encounter_at": ts}})
    await add_activity("admission", f"Admitted {p['full_name']} to {bed['ward_name']} · {bed['bed_number']} ({aid_code})", {"patient_id": data.patient_id, "admission_id": a["id"], "bed_id": data.bed_id})
    return await enrich_admission(a)


@api_router.post("/admissions/{aid}/transfer")
async def transfer_bed(aid: str, data: TransferRequest):
    a = await db.admissions.find_one({"id": aid})
    if not a:
        raise HTTPException(404, "Admission not found")
    if a["status"] != "Active":
        raise HTTPException(409, "Only active admissions can be transferred")
    if data.bed_id == a["bed_id"]:
        raise HTTPException(422, "Select a different bed to transfer")
    new_bed = await db.beds.find_one_and_update(
        {"id": data.bed_id, "org_id": ORG_ID, "status": "Available"},
        {"$set": {"status": "Occupied"}},
    )
    if not new_bed:
        cur = await db.beds.find_one({"id": data.bed_id}, {"_id": 0})
        raise HTTPException(409, f"Selected bed is no longer available (currently {cur['status'] if cur else 'unknown'}). Please choose another bed.")
    old_bed = await db.beds.find_one({"id": a["bed_id"]})
    ts = iso(now_utc())
    # mark old bed cleaning
    await db.beds.update_one({"id": a["bed_id"]}, {"$set": {"status": "Cleaning"}})
    # close old assignment, open new
    assignments = a.get("bed_assignments", [])
    if assignments:
        assignments[-1]["to"] = ts
    assignments.append({"bed_id": data.bed_id, "bed_number": new_bed["bed_number"], "ward_id": new_bed["ward_id"], "ward_name": new_bed["ward_name"], "from": ts, "to": None})
    await db.admissions.update_one({"id": aid}, {"$set": {"bed_id": data.bed_id, "ward_id": new_bed["ward_id"], "bed_assignments": assignments}})
    await db.bed_history.insert_one({"id": gen_uuid(), "bed_id": a["bed_id"], "event": f"Transfer out ({a['admission_id']})", "from": "Occupied", "to": "Cleaning", "at": ts})
    await db.bed_history.insert_one({"id": gen_uuid(), "bed_id": data.bed_id, "event": f"Transfer in ({a['admission_id']})", "from": "Available", "to": "Occupied", "at": ts})
    p = await db.patients.find_one({"id": a["patient_id"]}, {"_id": 0})
    await add_activity("bed_transfer", f"Transferred {p['full_name']} from {old_bed['bed_number']} to {new_bed['bed_number']}", {"patient_id": a["patient_id"], "admission_id": aid})
    a = await db.admissions.find_one({"id": aid})
    return await enrich_admission(a)


@api_router.post("/admissions/{aid}/discharge")
async def discharge(aid: str, data: DischargeRequest):
    a = await db.admissions.find_one({"id": aid})
    if not a:
        raise HTTPException(404, "Admission not found")
    if a["status"] != "Active":
        raise HTTPException(409, "Admission is not active")
    disc_dt = parse_dt(data.discharge_time)
    if disc_dt < parse_dt(a["admission_time"]):
        raise HTTPException(422, "Discharge time cannot be before admission time")
    if disc_dt > now_utc():
        raise HTTPException(422, "Discharge time cannot be in the future")
    ts = data.discharge_time
    assignments = a.get("bed_assignments", [])
    if assignments:
        assignments[-1]["to"] = ts
    await db.admissions.update_one({"id": aid}, {"$set": {"status": "Discharged", "discharge_time": ts, "discharge_note": data.note, "bed_assignments": assignments}})
    await db.beds.update_one({"id": a["bed_id"]}, {"$set": {"status": "Cleaning"}})
    bed = await db.beds.find_one({"id": a["bed_id"]})
    await db.bed_history.insert_one({"id": gen_uuid(), "bed_id": a["bed_id"], "event": f"Discharge ({a['admission_id']})", "from": "Occupied", "to": "Cleaning", "at": iso(now_utc())})
    p = await db.patients.find_one({"id": a["patient_id"]}, {"_id": 0})
    await add_activity("discharge", f"Discharged {p['full_name']} from {bed['bed_number']} ({a['admission_id']})", {"patient_id": a["patient_id"], "admission_id": aid})
    a = await db.admissions.find_one({"id": aid})
    return await enrich_admission(a)


# ----------------------------- Dashboard -----------------------------

@api_router.get("/dashboard/overview")
async def dashboard_overview():
    today = ist_today()
    start, end = ist_day_bounds(today)
    today_iso = today.isoformat()

    opd_today = await db.opd.find({"org_id": ORG_ID, "day": today_iso}, {"_id": 0}).to_list(500)
    opd_visits_today = len(opd_today)
    waiting = len([e for e in opd_today if e["status"] == "Waiting"])
    active_adm = await db.admissions.count_documents({"org_id": ORG_ID, "status": "Active"})
    discharges_today = await db.admissions.count_documents({"org_id": ORG_ID, "discharge_time": {"$gte": iso(start), "$lt": iso(end)}})

    beds = await db.beds.find({"org_id": ORG_ID}, {"_id": 0}).to_list(200)
    available = len([b for b in beds if b["status"] == "Available"])
    occupied = len([b for b in beds if b["status"] == "Occupied"])
    operational = len([b for b in beds if b["status"] in ("Available", "Occupied", "Cleaning")])
    occupancy = round(occupied / operational * 100) if operational else None

    kpis = {
        "opd_visits_today": opd_visits_today,
        "patients_waiting": waiting,
        "active_admissions": active_adm,
        "available_beds": available,
        "occupancy": occupancy,
        "occupancy_num": occupied,
        "occupancy_den": operational,
        "discharges_today": discharges_today,
    }

    # queue (today active)
    queue = [await enrich_opd(e) for e in opd_today if e["status"] in ACTIVE_OPD]
    queue.sort(key=lambda e: e.get("token", ""))

    # bed availability by ward
    wards = await db.wards.find({"org_id": ORG_ID}, {"_id": 0}).to_list(50)
    ward_avail = []
    for w in wards:
        wbeds = [b for b in beds if b["ward_id"] == w["id"]]
        ward_avail.append({
            "ward_id": w["id"],
            "ward_name": w["name"],
            "floor": w["floor"],
            "available": len([b for b in wbeds if b["status"] == "Available"]),
            "total": len(wbeds),
        })

    # active admissions
    adm_docs = await db.admissions.find({"org_id": ORG_ID, "status": "Active"}, {"_id": 0}).sort("admission_time", -1).to_list(100)
    admissions = [await enrich_admission(a) for a in adm_docs]

    # recent activity
    acts = await db.activity.find({"org_id": ORG_ID}, {"_id": 0}).sort("timestamp", -1).to_list(12)

    # 7-day OPD chart
    chart = []
    for i in range(6, -1, -1):
        d = today - timedelta(days=i)
        cnt = await db.opd.count_documents({"org_id": ORG_ID, "day": d.isoformat()})
        chart.append({"date": d.isoformat(), "label": d.strftime("%d %b"), "count": cnt})

    return {
        "kpis": kpis,
        "queue": queue,
        "ward_availability": ward_avail,
        "active_admissions": admissions,
        "recent_activity": acts,
        "opd_chart": chart,
        "org": {"name": ORG_NAME, "location": "Bengaluru", "today": today_iso},
    }


# ----------------------------- Seed -----------------------------

FIRST = ["Aarav", "Vivaan", "Aditya", "Vihaan", "Arjun", "Sai", "Reyansh", "Krishna", "Ishaan", "Rohan",
         "Ananya", "Diya", "Saanvi", "Aadhya", "Kavya", "Myra", "Anika", "Navya", "Ira", "Prisha",
         "Rahul", "Nikhil", "Karthik", "Suresh", "Meera", "Lakshmi", "Priya", "Deepa", "Varun", "Tara",
         "Manish", "Pooja", "Sanjay", "Neha", "Rakesh", "Shreya", "Gaurav", "Divya"]
LAST = ["Sharma", "Verma", "Iyer", "Nair", "Reddy", "Rao", "Menon", "Gupta", "Patel", "Desai",
        "Kulkarni", "Shetty", "Bhat", "Pillai", "Kumar", "Joshi", "Shah", "Naidu", "Hegde", "Mehta"]
CITIES = ["Bengaluru", "Mysuru", "Hubli", "Mangaluru", "Belagavi", "Tumakuru"]


async def seed():
    existing = await db.organizations.find_one({"id": ORG_ID})
    if existing:
        return
    logger.info("Seeding Minovis demo data...")
    rnd = random.Random(42)
    await db.organizations.insert_one({"id": ORG_ID, "name": ORG_NAME, "location": "Bengaluru", "timezone": "Asia/Kolkata"})

    # Departments & Doctors
    doctors = [
        {"name": "Dr. Arjun Rao", "department": "General Medicine"},
        {"name": "Dr. Kavya Nair", "department": "General Medicine"},
        {"name": "Dr. Rohan Mehta", "department": "Orthopedics"},
        {"name": "Dr. Sneha Iyer", "department": "Orthopedics"},
        {"name": "Dr. Vikram Desai", "department": "Pediatrics"},
    ]
    for d in doctors:
        d["id"] = gen_uuid()
        d["org_id"] = ORG_ID
        d["schedule"] = "09:00 - 17:00"
        await db.doctors.insert_one(dict(d))

    # Wards & Beds
    wards_def = [
        {"name": "General Ward A", "floor": "Ground floor", "floor_order": 0, "bed_type": "General", "beds": ["G-A01", "G-A02", "G-A03", "G-A04", "G-A05", "G-A06", "G-A07", "G-A08"]},
        {"name": "General Ward B", "floor": "First floor", "floor_order": 1, "bed_type": "General", "beds": ["G-B01", "G-B02", "G-B03", "G-B04", "G-B05", "G-B06"]},
        {"name": "Private Rooms", "floor": "First floor", "floor_order": 1, "bed_type": "Private", "beds": ["P-101", "P-102", "P-103", "P-104"]},
        {"name": "ICU", "floor": "Second floor", "floor_order": 2, "bed_type": "ICU", "beds": ["ICU-01", "ICU-02", "ICU-03", "ICU-04"]},
    ]
    status_map = {
        "G-A01": "Occupied", "G-A02": "Occupied", "G-A03": "Occupied", "G-A04": "Available",
        "G-A05": "Available", "G-A06": "Cleaning", "G-A07": "Maintenance", "G-A08": "Available",
        "G-B01": "Occupied", "G-B02": "Occupied", "G-B03": "Available", "G-B04": "Available",
        "G-B05": "Cleaning", "G-B06": "Available",
        "P-101": "Occupied", "P-102": "Available", "P-103": "Maintenance", "P-104": "Available",
        "ICU-01": "Occupied", "ICU-02": "Occupied", "ICU-03": "Occupied", "ICU-04": "Available",
    }
    bed_lookup = {}
    for w in wards_def:
        wid = gen_uuid()
        await db.wards.insert_one({"id": wid, "org_id": ORG_ID, "name": w["name"], "floor": w["floor"], "floor_order": w["floor_order"], "bed_type": w["bed_type"]})
        for bn in w["beds"]:
            bid = gen_uuid()
            bed = {"id": bid, "org_id": ORG_ID, "bed_number": bn, "ward_id": wid, "ward_name": w["name"],
                   "floor": w["floor"], "floor_order": w["floor_order"], "bed_type": w["bed_type"], "status": status_map[bn]}
            await db.beds.insert_one(dict(bed))
            bed_lookup[bn] = bed

    # Patients
    patient_docs = []
    used = set()
    for i in range(35):
        while True:
            fn = rnd.choice(FIRST)
            ln = rnd.choice(LAST)
            name = f"{fn} {ln}"
            if name not in used:
                used.add(name)
                break
        seq = await next_counter("patient")
        pid = f"MNV-{seq:06d}"
        age = rnd.randint(2, 82)
        dob = (ist_today().replace(year=ist_today().year - age) - timedelta(days=rnd.randint(0, 364))).isoformat()
        gender = rnd.choice(["Male", "Female", "Female", "Male", "Prefer not to say"])
        mobile = f"9{rnd.randint(100000000, 999999999)}"
        reg_days = rnd.randint(1, 400)
        created = iso(now_utc() - timedelta(days=reg_days))
        doc = {
            "id": gen_uuid(), "org_id": ORG_ID, "patient_id": pid, "full_name": name,
            "dob": dob, "gender": gender, "mobile": mobile,
            "email": f"{fn.lower()}.{ln.lower()}@example.com", "city": rnd.choice(CITIES),
            "emergency_contact_name": f"{rnd.choice(FIRST)} {ln}", "emergency_contact_phone": f"9{rnd.randint(100000000, 999999999)}",
            "created_at": created, "last_encounter_at": None, "synthetic": True,
        }
        await db.patients.insert_one(dict(doc))
        patient_docs.append(doc)

    all_docs = await db.doctors.find({"org_id": ORG_ID}, {"_id": 0}).to_list(10)

    # Active admissions for 9 occupied beds
    occupied_beds = [bn for bn, s in status_map.items() if s == "Occupied"]
    adm_patients = patient_docs[:len(occupied_beds)]
    for idx, bn in enumerate(occupied_beds):
        bed = bed_lookup[bn]
        p = adm_patients[idx]
        doc = rnd.choice([d for d in all_docs if d["department"] in ("General Medicine", "Orthopedics", "Pediatrics")])
        days_ago = rnd.randint(1, 8)
        adm_time = iso(now_utc() - timedelta(days=days_ago, hours=rnd.randint(0, 10)))
        seq = await next_counter("admission")
        aid_code = f"IPD-{seq:06d}"
        assign = {"bed_id": bed["id"], "bed_number": bn, "ward_id": bed["ward_id"], "ward_name": bed["ward_name"], "from": adm_time, "to": None}
        a = {
            "id": gen_uuid(), "org_id": ORG_ID, "admission_id": aid_code, "patient_id": p["id"],
            "doctor_id": doc["id"], "department": doc["department"], "ward_id": bed["ward_id"], "bed_id": bed["id"],
            "admission_time": adm_time, "discharge_time": None, "reason": rnd.choice(["Observation", "Post-operative care", "Fever management", "Fracture care", "Infection treatment"]),
            "status": "Active", "bed_assignments": [assign], "discharge_note": None, "created_at": adm_time,
        }
        await db.admissions.insert_one(dict(a))
        await db.patients.update_one({"id": p["id"]}, {"$set": {"last_encounter_at": adm_time}})
        await db.bed_history.insert_one({"id": gen_uuid(), "bed_id": bed["id"], "event": f"Assigned to {p['full_name']} ({aid_code})", "from": "Available", "to": "Occupied", "at": adm_time})
        await db.activity.insert_one({"id": gen_uuid(), "org_id": ORG_ID, "kind": "admission", "description": f"Admitted {p['full_name']} to {bed['ward_name']} · {bn} ({aid_code})", "refs": {"patient_id": p["id"]}, "timestamp": adm_time})

    # Historical discharged admissions
    disc_patients = patient_docs[len(occupied_beds):len(occupied_beds) + 6]
    for p in disc_patients:
        doc = rnd.choice(all_docs)
        start_days = rnd.randint(10, 40)
        adm_time = iso(now_utc() - timedelta(days=start_days))
        disc_time = iso(now_utc() - timedelta(days=start_days - rnd.randint(2, 6)))
        bn = rnd.choice(list(status_map.keys()))
        bed = bed_lookup[bn]
        seq = await next_counter("admission")
        aid_code = f"IPD-{seq:06d}"
        a = {
            "id": gen_uuid(), "org_id": ORG_ID, "admission_id": aid_code, "patient_id": p["id"],
            "doctor_id": doc["id"], "department": doc["department"], "ward_id": bed["ward_id"], "bed_id": bed["id"],
            "admission_time": adm_time, "discharge_time": disc_time, "reason": "Recovered",
            "status": "Discharged", "bed_assignments": [{"bed_id": bed["id"], "bed_number": bn, "ward_id": bed["ward_id"], "ward_name": bed["ward_name"], "from": adm_time, "to": disc_time}],
            "discharge_note": "Discharged in stable condition.", "created_at": adm_time,
        }
        await db.admissions.insert_one(dict(a))

    # OPD records across last 7 days + today
    def mk_opd(day_date, idx_token, patient, doc, mode, sched_dt, status, reason):
        ts_created = iso(datetime.combine(day_date, dtime(8, 0), tzinfo=IST).astimezone(timezone.utc) + timedelta(minutes=idx_token * 5))
        checkin = None
        history = [{"status": "Scheduled" if mode == "appointment" else "Waiting", "at": ts_created}]
        if mode == "walk-in":
            checkin = ts_created
        return {
            "id": gen_uuid(), "org_id": ORG_ID, "token": f"OPD-{idx_token:03d}", "day": day_date.isoformat(),
            "patient_id": patient["id"], "doctor_id": doc["id"], "department": doc["department"],
            "visit_mode": mode, "scheduled_time": iso(sched_dt) if sched_dt else None,
            "checkin_time": checkin, "reason": reason, "status": status, "status_history": history, "created_at": ts_created,
        }

    reasons = ["Fever and cold", "Follow-up", "Joint pain", "Routine checkup", "Skin rash", "Headache", "Stomach ache", "Vaccination"]
    today = ist_today()
    # past 6 days: completed visits
    for i in range(6, 0, -1):
        d = today - timedelta(days=i)
        token = 0
        slot_tracker = {}
        n = rnd.randint(4, 9)
        for _ in range(n):
            token += 1
            p = rnd.choice(patient_docs)
            doc = rnd.choice(all_docs)
            mode = rnd.choice(["appointment", "walk-in"])
            sched_dt = None
            if mode == "appointment":
                hour = rnd.randint(9, 16)
                minute = rnd.choice([0, 20, 40])
                key = (doc["id"], hour, minute)
                if key in slot_tracker:
                    continue
                slot_tracker[key] = True
                sched_dt = datetime.combine(d, dtime(hour, minute), tzinfo=IST)
            e = mk_opd(d, token, p, doc, mode, sched_dt, "Completed", rnd.choice(reasons))
            await db.opd.insert_one(e)

    # today: mix of appointments and walk-ins at various statuses
    token = 0
    slot_tracker = {}
    now = now_utc()
    today_specs = [
        ("appointment", 9, 0, "Completed"),
        ("appointment", 9, 20, "Completed"),
        ("walk-in", None, None, "Completed"),
        ("appointment", 10, 0, "In consultation"),
        ("walk-in", None, None, "In consultation"),
        ("walk-in", None, None, "Waiting"),
        ("appointment", 11, 0, "Waiting"),
        ("walk-in", None, None, "Waiting"),
        ("appointment", 14, 0, "Scheduled"),
        ("appointment", 15, 0, "Scheduled"),
        ("appointment", 16, 0, "Scheduled"),
    ]
    for mode, hour, minute, status in today_specs:
        token += 1
        p = rnd.choice(patient_docs)
        doc = rnd.choice(all_docs)
        sched_dt = None
        if mode == "appointment":
            sched_dt = datetime.combine(today, dtime(hour, minute), tzinfo=IST)
            # for scheduled future ones ensure future; if already past, push to a Completed-style is fine
            key = (doc["id"], hour, minute)
            if key in slot_tracker:
                doc = rnd.choice([x for x in all_docs if x["id"] != doc["id"]])
            slot_tracker[(doc["id"], hour, minute)] = True
        e = mk_opd(today, token, p, doc, mode, sched_dt, status, rnd.choice(reasons))
        await db.opd.insert_one(e)
        await db.patients.update_one({"id": p["id"]}, {"$set": {"last_encounter_at": e["created_at"]}})

    # a few upcoming appointments (tomorrow)
    tomorrow = today + timedelta(days=1)
    token = 0
    for hour in [9, 10, 11]:
        token += 1
        p = rnd.choice(patient_docs)
        doc = rnd.choice(all_docs)
        sched_dt = datetime.combine(tomorrow, dtime(hour, 0), tzinfo=IST)
        e = mk_opd(tomorrow, token, p, doc, "appointment", sched_dt, "Scheduled", rnd.choice(reasons))
        await db.opd.insert_one(e)

    logger.info("Seeding complete.")


@app.on_event("startup")
async def on_startup():
    await seed()


app.include_router(api_router)

app.add_middleware(
    CORSMiddleware,
    allow_credentials=True,
    allow_origins=os.environ.get('CORS_ORIGINS', '*').split(','),
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.on_event("shutdown")
async def shutdown_db_client():
    client.close()
