import os
import logging
from fastapi import FastAPI, Request, Depends, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse, FileResponse, JSONResponse, RedirectResponse
from fastapi.exceptions import RequestValidationError
from sqlalchemy.orm import Session
from typing import List, Dict, Optional
from pydantic import BaseModel

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

from backend.database import engine, Base, get_db
from backend.routers import roster, units, photos, roster_edit, roster_rename, dashboard, dashboard_tabs, activity, slmm, slm_ui, slm_dashboard, seismo_dashboard, sfm, projects, project_locations, scheduler, modem_dashboard
from backend.services.snapshot import emit_status_snapshot
from backend.models import IgnoredUnit
from backend.utils.timezone import get_user_timezone

# Create database tables
Base.metadata.create_all(bind=engine)

# Read environment (development or production)
ENVIRONMENT = os.getenv("ENVIRONMENT", "production")

# Initialize FastAPI app
VERSION = "0.15.0"
if ENVIRONMENT == "development":
    _build = os.getenv("BUILD_NUMBER", "0")
    if _build and _build != "0":
        VERSION = f"{VERSION}-{_build}"
app = FastAPI(
    title="Seismo Fleet Manager",
    description="Backend API for managing seismograph fleet status",
    version=VERSION
)

# Add validation error handler to log details
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    logger.error(f"Validation error on {request.url}: {exc.errors()}")
    logger.error(f"Body: {await request.body()}")
    return JSONResponse(
        status_code=400,
        content={"detail": exc.errors()}
    )

# Configure CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Mount static files
app.mount("/static", StaticFiles(directory="backend/static"), name="static")

# Use shared templates configuration with timezone filters
from backend.templates_config import templates

# Client-portal auth: an unauthenticated portal request renders the access page
# (HTML routes) or returns 401 JSON (/portal/api/* routes). Centralized so every
# portal route can simply Depends(get_current_client).
from backend.portal_auth import PortalAuthError

@app.exception_handler(PortalAuthError)
async def portal_auth_handler(request: Request, exc: PortalAuthError):
    if request.url.path.startswith("/portal/api"):
        return JSONResponse(status_code=401, content={"detail": "Not authenticated"})
    return templates.TemplateResponse(
        "portal/access_required.html",
        {"request": request, "reason": "required"},
        status_code=401,
    )

# Add custom context processor to inject environment variable into all templates
@app.middleware("http")
async def add_environment_to_context(request: Request, call_next):
    """Middleware to add environment variable to request state"""
    request.state.environment = ENVIRONMENT
    response = await call_next(request)
    return response

# Operator auth — deny-by-default gate over the whole internal app. Governed by
# OPERATOR_AUTH_ENABLED (default off → behaves exactly as today). See
# docs/superpowers/specs/2026-06-17-operator-auth-design.md.
from backend.operator_auth import operator_gate
app.middleware("http")(operator_gate)

from backend.routers import operator_auth_routes
app.include_router(operator_auth_routes.router)

from backend.routers import operator_users
app.include_router(operator_users.router)

# Override TemplateResponse to include environment and version in context
original_template_response = templates.TemplateResponse
def custom_template_response(name, context=None, *args, **kwargs):
    if context is None:
        context = {}
    context["environment"] = ENVIRONMENT
    context["version"] = VERSION
    return original_template_response(name, context, *args, **kwargs)
templates.TemplateResponse = custom_template_response

# Include API routers
app.include_router(roster.router)
app.include_router(units.router)
app.include_router(photos.router)
app.include_router(roster_edit.router)
app.include_router(roster_rename.router)
app.include_router(dashboard.router)
app.include_router(dashboard_tabs.router)
app.include_router(activity.router)
app.include_router(slmm.router)
app.include_router(slm_ui.router)
app.include_router(slm_dashboard.router)
app.include_router(seismo_dashboard.router)

# Client portal (read-only, scoped client view) — see docs/CLIENT_PORTAL.md
from backend.routers import portal
app.include_router(portal.router)
app.include_router(sfm.router)
app.include_router(modem_dashboard.router)

from backend.routers import settings
app.include_router(settings.router)

from backend.routers import watcher_manager
app.include_router(watcher_manager.router)

from backend.routers import admin_modules
app.include_router(admin_modules.router)

from backend.routers import deployment_history
app.include_router(deployment_history.router)

from backend.routers import pending_deployments
app.include_router(pending_deployments.router)

# Projects system routers
app.include_router(projects.router)
app.include_router(project_locations.router)
app.include_router(scheduler.router)

# Report templates router
from backend.routers import report_templates
app.include_router(report_templates.router)

# Metadata-backfill admin router (Phase 5a)
from backend.routers import metadata_backfill
app.include_router(metadata_backfill.router)

# Alerts router
from backend.routers import alerts
app.include_router(alerts.router)

# Recurring schedules router
from backend.routers import recurring_schedules
app.include_router(recurring_schedules.router)

# Fleet Calendar router
from backend.routers import fleet_calendar
app.include_router(fleet_calendar.router)

# Deployment Records router
from backend.routers import deployments
app.include_router(deployments.router)

# Calibration sync router (SFM-driven cal date updates)
from backend.routers import calibration
app.include_router(calibration.router)

# Nightly sound-report pipeline (manual triggers; scheduled tick reuses run_nightly_report)
from backend.routers import reports
app.include_router(reports.router)

# Start scheduler service and device status monitor on application startup
from backend.services.scheduler import start_scheduler, stop_scheduler
from backend.services.device_status_monitor import start_device_status_monitor, stop_device_status_monitor
from backend.services.calibration_sync import get_calibration_sync_scheduler

@app.on_event("startup")
async def startup_event():
    """Initialize services on app startup"""
    logger.info("Starting scheduler service...")
    await start_scheduler()
    logger.info("Scheduler service started")

    logger.info("Starting device status monitor...")
    await start_device_status_monitor()
    logger.info("Device status monitor started")

    logger.info("Starting calibration sync scheduler...")
    get_calibration_sync_scheduler().start()
    logger.info("Calibration sync scheduler started")

@app.on_event("shutdown")
def shutdown_event():
    """Clean up services on app shutdown"""
    logger.info("Stopping device status monitor...")
    stop_device_status_monitor()
    logger.info("Device status monitor stopped")

    logger.info("Stopping scheduler service...")
    stop_scheduler()
    logger.info("Scheduler service stopped")

    logger.info("Stopping calibration sync scheduler...")
    get_calibration_sync_scheduler().stop()
    logger.info("Calibration sync scheduler stopped")


# Legacy routes from the original backend
from backend import routes as legacy_routes
app.include_router(legacy_routes.router)


# HTML page routes
@app.get("/", response_class=HTMLResponse)
async def dashboard(request: Request):
    """Dashboard home page"""
    return templates.TemplateResponse("dashboard.html", {"request": request})


@app.get("/roster", response_class=HTMLResponse)
async def roster_page(request: Request):
    """Fleet roster page"""
    return templates.TemplateResponse("roster.html", {"request": request})


@app.get("/unit/{unit_id}", response_class=HTMLResponse)
async def unit_detail_page(request: Request, unit_id: str):
    """Unit detail page"""
    return templates.TemplateResponse("unit_detail.html", {
        "request": request,
        "unit_id": unit_id
    })


@app.get("/settings", response_class=HTMLResponse)
async def settings_page(request: Request):
    """Settings page for roster management"""
    return templates.TemplateResponse("settings.html", {"request": request})


@app.get("/sound-level-meters", response_class=HTMLResponse)
async def sound_level_meters_page(request: Request):
    """Sound Level Meters management dashboard"""
    return templates.TemplateResponse("sound_level_meters.html", {"request": request})


@app.get("/slm/{unit_id}", response_class=HTMLResponse)
async def slm_legacy_dashboard(
    request: Request,
    unit_id: str,
    from_project: Optional[str] = None,
    from_nrl: Optional[str] = None,
    db: Session = Depends(get_db)
):
    """Legacy SLM control center dashboard for a specific unit"""
    # Get project details if from_project is provided
    project = None
    if from_project:
        from backend.models import Project
        project = db.query(Project).filter_by(id=from_project).first()

    # Get NRL location details if from_nrl is provided
    nrl_location = None
    if from_nrl:
        from backend.models import NRLLocation
        nrl_location = db.query(NRLLocation).filter_by(id=from_nrl).first()

    return templates.TemplateResponse("slm_legacy_dashboard.html", {
        "request": request,
        "unit_id": unit_id,
        "from_project": from_project,
        "from_nrl": from_nrl,
        "project": project,
        "nrl_location": nrl_location
    })


@app.get("/seismographs", response_class=HTMLResponse)
async def seismographs_page(request: Request):
    """Seismographs management dashboard"""
    return templates.TemplateResponse("seismographs.html", {"request": request})


@app.get("/sfm", response_class=HTMLResponse)
async def sfm_page(request: Request):
    """SFM live event data and device control dashboard"""
    return templates.TemplateResponse("sfm.html", {"request": request})


@app.get("/settings/developer/metadata-backfill", response_class=HTMLResponse)
async def metadata_backfill_wizard_page(request: Request):
    """Wizard for auto-creating projects/locations/assignments from
    operator-typed BW event metadata (Phase 5a)."""
    return templates.TemplateResponse("admin/metadata_backfill.html", {"request": request})


@app.get("/settings/developer/project-tidy", response_class=HTMLResponse)
async def project_tidy_page(request: Request):
    """Tidy duplicate-looking projects: detect by fuzzy name match, merge
    by clicking through pairs (Phase 5b)."""
    return templates.TemplateResponse("admin/project_tidy.html", {"request": request})


@app.get("/tools", response_class=HTMLResponse)
async def tools_page(request: Request):
    """Tools / workflow hub.  Active operator workflows (device pairing,
    project tidy, metadata backfill, future swap detection, report
    generators) all live here in card form."""
    return templates.TemplateResponse("tools.html", {"request": request})


@app.get("/deploy", response_class=HTMLResponse)
async def deploy_page(request: Request):
    """Mobile-first field-capture wizard.  Pick a seismograph, snap a
    photo of the install, optionally add a memo — drop into the pending
    hopper for later classification."""
    return templates.TemplateResponse("deploy.html", {"request": request})


@app.get("/tools/pending-deployments", response_class=HTMLResponse)
async def pending_deployments_page(request: Request):
    """List of field captures awaiting classification, plus filters for
    historical assigned / cancelled rows.  Operators promote a capture
    into a real UnitAssignment from here."""
    return templates.TemplateResponse("admin/pending_deployments.html", {"request": request})


@app.get("/tools/unit-swap", response_class=HTMLResponse)
async def unit_swap_page(request: Request):
    """Mobile-first wizard for swapping a vibration unit (and optionally its
    modem) at a monitoring location.  Pick project → location → incoming
    unit → modem decision → confirm → optional photo of the new install."""
    return templates.TemplateResponse("admin/unit_swap.html", {"request": request})


@app.get("/modems", response_class=HTMLResponse)
async def modems_page(request: Request):
    """Field modems management dashboard"""
    return templates.TemplateResponse("modems.html", {"request": request})


@app.get("/pair-devices", response_class=HTMLResponse)
async def pair_devices_page(request: Request, db: Session = Depends(get_db)):
    """
    Device pairing page - two-column layout for pairing recorders with modems.
    """
    from backend.models import RosterUnit

    # Get all non-retired recorders (seismographs and SLMs)
    recorders = db.query(RosterUnit).filter(
        RosterUnit.retired == False,
        RosterUnit.device_type.in_(["seismograph", "slm", None])  # None defaults to seismograph
    ).order_by(RosterUnit.id).all()

    # Get all non-retired modems
    modems = db.query(RosterUnit).filter(
        RosterUnit.retired == False,
        RosterUnit.device_type == "modem"
    ).order_by(RosterUnit.id).all()

    # Build existing pairings list
    pairings = []
    for recorder in recorders:
        if recorder.deployed_with_modem_id:
            modem = next((m for m in modems if m.id == recorder.deployed_with_modem_id), None)
            pairings.append({
                "recorder_id": recorder.id,
                "recorder_type": (recorder.device_type or "seismograph").upper(),
                "modem_id": recorder.deployed_with_modem_id,
                "modem_ip": modem.ip_address if modem else None
            })

    # Convert to dicts for template
    recorders_data = [
        {
            "id": r.id,
            "device_type": r.device_type or "seismograph",
            "deployed": r.deployed,
            "deployed_with_modem_id": r.deployed_with_modem_id
        }
        for r in recorders
    ]

    modems_data = [
        {
            "id": m.id,
            "deployed": m.deployed,
            "deployed_with_unit_id": m.deployed_with_unit_id,
            "ip_address": m.ip_address,
            "phone_number": m.phone_number
        }
        for m in modems
    ]

    return templates.TemplateResponse("pair_devices.html", {
        "request": request,
        "recorders": recorders_data,
        "modems": modems_data,
        "pairings": pairings
    })


@app.get("/projects", response_class=HTMLResponse)
async def projects_page(request: Request):
    """Projects management and overview"""
    return templates.TemplateResponse("projects/overview.html", {"request": request})


@app.get("/projects/{project_id}", response_class=HTMLResponse)
async def project_detail_page(request: Request, project_id: str):
    """Project detail dashboard"""
    return templates.TemplateResponse("projects/detail.html", {
        "request": request,
        "project_id": project_id,
    })


@app.get("/projects/{project_id}/portal-preview")
async def project_portal_preview(project_id: str, db: Session = Depends(get_db)):
    """Operator testing shortcut: open this project's client portal (no CLI)."""
    from backend.models import Project
    from backend.portal_auth import mint_portal_session, make_session_cookie, COOKIE_NAME, COOKIE_MAX_AGE, COOKIE_SECURE
    project = db.query(Project).filter_by(id=project_id).first()
    if not project:
        return JSONResponse(status_code=404, content={"detail": "Project not found"})
    token_id = mint_portal_session(project, db)
    resp = RedirectResponse(url="/portal", status_code=303)
    resp.set_cookie(COOKIE_NAME, make_session_cookie(token_id),
                    max_age=COOKIE_MAX_AGE, httponly=True, samesite="lax", secure=COOKIE_SECURE)
    return resp


@app.get("/projects/{project_id}/portal-access")
async def project_portal_access_state(project_id: str, request: Request, db: Session = Depends(get_db)):
    """Current portal-access state for the operator panel."""
    from backend.models import Project
    p = db.query(Project).filter_by(id=project_id).first()
    if not p:
        return JSONResponse(status_code=404, content={"detail": "Project not found"})
    link_url = (str(request.base_url).rstrip("/") + f"/portal/p/{p.portal_link_token}") \
        if (p.portal_enabled and p.portal_link_token) else None
    return {"enabled": bool(p.portal_enabled), "has_password": bool(p.portal_password_hash),
            "link_url": link_url}


@app.post("/projects/{project_id}/portal-access/enable")
async def project_portal_access_enable(project_id: str, request: Request, db: Session = Depends(get_db)):
    """Turn the portal on; mint a link token if one doesn't exist yet."""
    import secrets
    from backend.models import Project
    p = db.query(Project).filter_by(id=project_id).first()
    if not p:
        return JSONResponse(status_code=404, content={"detail": "Project not found"})
    if not p.portal_link_token:
        p.portal_link_token = secrets.token_urlsafe(24)
    p.portal_enabled = True
    db.commit()
    link_url = str(request.base_url).rstrip("/") + f"/portal/p/{p.portal_link_token}"
    return {"enabled": True, "has_password": bool(p.portal_password_hash), "link_url": link_url}


@app.post("/projects/{project_id}/portal-access/password")
async def project_portal_access_password(project_id: str, db: Session = Depends(get_db)):
    """Generate a fresh strong password, store its hash, return the raw once."""
    from backend.models import Project
    from backend.auth_passwords import hash_password, generate_password
    p = db.query(Project).filter_by(id=project_id).first()
    if not p:
        return JSONResponse(status_code=404, content={"detail": "Project not found"})
    raw = generate_password()
    p.portal_password_hash = hash_password(raw)
    db.commit()
    return {"password": raw}


@app.post("/projects/{project_id}/portal-access/disable")
async def project_portal_access_disable(project_id: str, db: Session = Depends(get_db)):
    """Turn the portal off and rotate the link token (kills the old link)."""
    import secrets
    from backend.models import Project
    p = db.query(Project).filter_by(id=project_id).first()
    if not p:
        return JSONResponse(status_code=404, content={"detail": "Project not found"})
    p.portal_enabled = False
    p.portal_link_token = secrets.token_urlsafe(24)  # rotate so the old link 404s
    db.commit()
    return {"enabled": False}


@app.get("/projects/{project_id}/nrl/{location_id}", response_class=HTMLResponse)
async def nrl_detail_page(
    request: Request,
    project_id: str,
    location_id: str,
    db: Session = Depends(get_db)
):
    """NRL (Noise Recording Location) detail page with tabs"""
    from backend.models import Project, MonitoringLocation, UnitAssignment, RosterUnit, MonitoringSession, DataFile
    from sqlalchemy import and_

    # Get project
    project = db.query(Project).filter_by(id=project_id).first()
    if not project:
        return templates.TemplateResponse("404.html", {
            "request": request,
            "message": "Project not found"
        }, status_code=404)

    # Get location
    location = db.query(MonitoringLocation).filter_by(
        id=location_id,
        project_id=project_id
    ).first()

    if not location:
        return templates.TemplateResponse("404.html", {
            "request": request,
            "message": "Location not found"
        }, status_code=404)

    # Get active assignment
    assignment = db.query(UnitAssignment).filter(
        and_(
            UnitAssignment.location_id == location_id,
            UnitAssignment.status == "active"
        )
    ).first()

    assigned_unit = None
    assigned_modem = None
    if assignment:
        assigned_unit = db.query(RosterUnit).filter_by(id=assignment.unit_id).first()
        if assigned_unit and assigned_unit.deployed_with_modem_id:
            assigned_modem = db.query(RosterUnit).filter_by(id=assigned_unit.deployed_with_modem_id).first()

    # Get session count
    session_count = db.query(MonitoringSession).filter_by(location_id=location_id).count()

    # Get file count (DataFile links to session, not directly to location)
    file_count = db.query(DataFile).join(
        MonitoringSession,
        DataFile.session_id == MonitoringSession.id
    ).filter(MonitoringSession.location_id == location_id).count()

    # Check for active session
    active_session = db.query(MonitoringSession).filter(
        and_(
            MonitoringSession.location_id == location_id,
            MonitoringSession.status == "recording"
        )
    ).first()

    # Parse connection_mode from location_metadata JSON
    import json as _json
    connection_mode = "connected"
    try:
        meta = _json.loads(location.location_metadata or "{}")
        connection_mode = meta.get("connection_mode", "connected")
    except Exception:
        pass

    template = "vibration_location_detail.html" if location.location_type == "vibration" else "nrl_detail.html"
    return templates.TemplateResponse(template, {
        "request": request,
        "project_id": project_id,
        "location_id": location_id,
        "project": project,
        "location": location,
        "assignment": assignment,
        "assigned_unit": assigned_unit,
        "assigned_modem": assigned_modem,
        "session_count": session_count,
        "file_count": file_count,
        "active_session": active_session,
        "connection_mode": connection_mode,
    })


# ===== PWA ROUTES =====

@app.get("/sw.js")
async def service_worker():
    """Serve service worker with proper headers for PWA"""
    return FileResponse(
        "backend/static/sw.js",
        media_type="application/javascript",
        headers={
            "Service-Worker-Allowed": "/",
            "Cache-Control": "no-cache"
        }
    )


@app.get("/offline-db.js")
async def offline_db_script():
    """Serve offline database script"""
    return FileResponse(
        "backend/static/offline-db.js",
        media_type="application/javascript",
        headers={"Cache-Control": "no-cache"}
    )


# Pydantic model for sync edits
class EditItem(BaseModel):
    id: int
    unitId: str
    changes: Dict
    timestamp: int


class SyncEditsRequest(BaseModel):
    edits: List[EditItem]


@app.post("/api/sync-edits")
async def sync_edits(request: SyncEditsRequest, db: Session = Depends(get_db)):
    """Process offline edit queue and sync to database"""
    from backend.models import RosterUnit

    results = []
    synced_ids = []

    for edit in request.edits:
        try:
            # Find the unit
            unit = db.query(RosterUnit).filter_by(id=edit.unitId).first()

            if not unit:
                results.append({
                    "id": edit.id,
                    "status": "error",
                    "reason": f"Unit {edit.unitId} not found"
                })
                continue

            # Apply changes
            for key, value in edit.changes.items():
                if hasattr(unit, key):
                    # Handle boolean conversions
                    if key in ['deployed', 'retired']:
                        setattr(unit, key, value in ['true', True, 'True', '1', 1])
                    else:
                        setattr(unit, key, value if value != '' else None)

            db.commit()

            results.append({
                "id": edit.id,
                "status": "success"
            })
            synced_ids.append(edit.id)

        except Exception as e:
            db.rollback()
            results.append({
                "id": edit.id,
                "status": "error",
                "reason": str(e)
            })

    synced_count = len(synced_ids)

    return JSONResponse({
        "synced": synced_count,
        "total": len(request.edits),
        "synced_ids": synced_ids,
        "results": results
    })


@app.get("/partials/roster-deployed", response_class=HTMLResponse)
async def roster_deployed_partial(request: Request):
    """Partial template for deployed units tab"""
    from datetime import datetime
    snapshot = emit_status_snapshot()

    units_list = []
    for unit_id, unit_data in snapshot["active"].items():
        units_list.append({
            "id": unit_id,
            "status": unit_data.get("status", "Unknown"),
            "age": unit_data.get("age", "N/A"),
            "last_seen": unit_data.get("last", "Never"),
            "deployed": unit_data.get("deployed", False),
            "note": unit_data.get("note", ""),
            "device_type": unit_data.get("device_type", "seismograph"),
            "address": unit_data.get("address", ""),
            "coordinates": unit_data.get("coordinates", ""),
            "project_id": unit_data.get("project_id", ""),
            "last_calibrated": unit_data.get("last_calibrated"),
            "next_calibration_due": unit_data.get("next_calibration_due"),
            "deployed_with_modem_id": unit_data.get("deployed_with_modem_id"),
            "ip_address": unit_data.get("ip_address"),
            "phone_number": unit_data.get("phone_number"),
            "hardware_model": unit_data.get("hardware_model"),
        })

    # Sort by status priority (Missing > Pending > OK) then by ID
    status_priority = {"Missing": 0, "Pending": 1, "OK": 2}
    units_list.sort(key=lambda x: (status_priority.get(x["status"], 3), x["id"]))

    return templates.TemplateResponse("partials/roster_table.html", {
        "request": request,
        "units": units_list,
        "timestamp": datetime.now().strftime("%H:%M:%S")
    })


@app.get("/partials/roster-benched", response_class=HTMLResponse)
async def roster_benched_partial(request: Request):
    """Partial template for benched units tab"""
    from datetime import datetime
    snapshot = emit_status_snapshot()

    units_list = []
    for unit_id, unit_data in snapshot["benched"].items():
        units_list.append({
            "id": unit_id,
            "status": unit_data.get("status", "N/A"),
            "age": unit_data.get("age", "N/A"),
            "last_seen": unit_data.get("last", "Never"),
            "deployed": unit_data.get("deployed", False),
            "note": unit_data.get("note", ""),
            "device_type": unit_data.get("device_type", "seismograph"),
            "address": unit_data.get("address", ""),
            "coordinates": unit_data.get("coordinates", ""),
            "project_id": unit_data.get("project_id", ""),
            "last_calibrated": unit_data.get("last_calibrated"),
            "next_calibration_due": unit_data.get("next_calibration_due"),
            "deployed_with_modem_id": unit_data.get("deployed_with_modem_id"),
            "ip_address": unit_data.get("ip_address"),
            "phone_number": unit_data.get("phone_number"),
            "hardware_model": unit_data.get("hardware_model"),
        })

    # Sort by ID
    units_list.sort(key=lambda x: x["id"])

    return templates.TemplateResponse("partials/roster_table.html", {
        "request": request,
        "units": units_list,
        "timestamp": datetime.now().strftime("%H:%M:%S")
    })


@app.get("/partials/roster-retired", response_class=HTMLResponse)
async def roster_retired_partial(request: Request):
    """Partial template for retired units tab"""
    from datetime import datetime
    snapshot = emit_status_snapshot()

    units_list = []
    for unit_id, unit_data in snapshot["retired"].items():
        units_list.append({
            "id": unit_id,
            "status": unit_data["status"],
            "age": unit_data["age"],
            "last_seen": unit_data["last"],
            "deployed": unit_data["deployed"],
            "note": unit_data.get("note", ""),
            "device_type": unit_data.get("device_type", "seismograph"),
            "last_calibrated": unit_data.get("last_calibrated"),
            "next_calibration_due": unit_data.get("next_calibration_due"),
            "deployed_with_modem_id": unit_data.get("deployed_with_modem_id"),
            "ip_address": unit_data.get("ip_address"),
            "phone_number": unit_data.get("phone_number"),
            "hardware_model": unit_data.get("hardware_model"),
        })

    # Sort by ID
    units_list.sort(key=lambda x: x["id"])

    return templates.TemplateResponse("partials/retired_table.html", {
        "request": request,
        "units": units_list,
        "timestamp": datetime.now().strftime("%H:%M:%S")
    })


@app.get("/partials/roster-ignored", response_class=HTMLResponse)
async def roster_ignored_partial(request: Request, db: Session = Depends(get_db)):
    """Partial template for ignored units tab"""
    from datetime import datetime

    ignored = db.query(IgnoredUnit).all()
    ignored_list = []
    for unit in ignored:
        ignored_list.append({
            "id": unit.id,
            "reason": unit.reason or "",
            "ignored_at": unit.ignored_at.strftime("%Y-%m-%d %H:%M:%S") if unit.ignored_at else "Unknown"
        })

    # Sort by ID
    ignored_list.sort(key=lambda x: x["id"])

    return templates.TemplateResponse("partials/ignored_table.html", {
        "request": request,
        "ignored_units": ignored_list,
        "timestamp": datetime.now().strftime("%H:%M:%S")
    })


@app.get("/partials/unknown-emitters", response_class=HTMLResponse)
async def unknown_emitters_partial(request: Request):
    """Partial template for unknown emitters (HTMX)"""
    snapshot = emit_status_snapshot()

    unknown_list = []
    for unit_id, unit_data in snapshot.get("unknown", {}).items():
        unknown_list.append({
            "id": unit_id,
            "status": unit_data["status"],
            "age": unit_data["age"],
            "fname": unit_data.get("fname", ""),
        })

    # Sort by ID
    unknown_list.sort(key=lambda x: x["id"])

    return templates.TemplateResponse("partials/unknown_emitters.html", {
        "request": request,
        "unknown_units": unknown_list
    })


@app.get("/partials/devices-all", response_class=HTMLResponse)
async def devices_all_partial(request: Request):
    """Unified partial template for ALL devices with comprehensive filtering support"""
    from datetime import datetime
    snapshot = emit_status_snapshot()

    units_list = []

    # Add deployed/active units
    for unit_id, unit_data in snapshot["active"].items():
        units_list.append({
            "id": unit_id,
            "status": unit_data.get("status", "Unknown"),
            "age": unit_data.get("age", "N/A"),
            "last_seen": unit_data.get("last", "Never"),
            "deployed": True,
            "retired": False,
            "out_for_calibration": False,
            "ignored": False,
            "note": unit_data.get("note", ""),
            "device_type": unit_data.get("device_type", "seismograph"),
            "address": unit_data.get("address", ""),
            "coordinates": unit_data.get("coordinates", ""),
            "project_id": unit_data.get("project_id", ""),
            "last_calibrated": unit_data.get("last_calibrated"),
            "next_calibration_due": unit_data.get("next_calibration_due"),
            "deployed_with_modem_id": unit_data.get("deployed_with_modem_id"),
            "deployed_with_unit_id": unit_data.get("deployed_with_unit_id"),
            "ip_address": unit_data.get("ip_address"),
            "phone_number": unit_data.get("phone_number"),
            "hardware_model": unit_data.get("hardware_model"),
        })

    # Add benched units
    for unit_id, unit_data in snapshot["benched"].items():
        units_list.append({
            "id": unit_id,
            "status": unit_data.get("status", "N/A"),
            "age": unit_data.get("age", "N/A"),
            "last_seen": unit_data.get("last", "Never"),
            "deployed": False,
            "retired": False,
            "out_for_calibration": False,
            "ignored": False,
            "note": unit_data.get("note", ""),
            "device_type": unit_data.get("device_type", "seismograph"),
            "address": unit_data.get("address", ""),
            "coordinates": unit_data.get("coordinates", ""),
            "project_id": unit_data.get("project_id", ""),
            "last_calibrated": unit_data.get("last_calibrated"),
            "next_calibration_due": unit_data.get("next_calibration_due"),
            "deployed_with_modem_id": unit_data.get("deployed_with_modem_id"),
            "deployed_with_unit_id": unit_data.get("deployed_with_unit_id"),
            "ip_address": unit_data.get("ip_address"),
            "phone_number": unit_data.get("phone_number"),
            "hardware_model": unit_data.get("hardware_model"),
        })

    # Add allocated units
    for unit_id, unit_data in snapshot.get("allocated", {}).items():
        units_list.append({
            "id": unit_id,
            "status": "Allocated",
            "age": "N/A",
            "last_seen": "N/A",
            "deployed": False,
            "retired": False,
            "out_for_calibration": False,
            "allocated": True,
            "allocated_to_project_id": unit_data.get("allocated_to_project_id", ""),
            "ignored": False,
            "note": unit_data.get("note", ""),
            "device_type": unit_data.get("device_type", "seismograph"),
            "address": unit_data.get("address", ""),
            "coordinates": unit_data.get("coordinates", ""),
            "project_id": unit_data.get("project_id", ""),
            "last_calibrated": unit_data.get("last_calibrated"),
            "next_calibration_due": unit_data.get("next_calibration_due"),
            "deployed_with_modem_id": unit_data.get("deployed_with_modem_id"),
            "deployed_with_unit_id": unit_data.get("deployed_with_unit_id"),
            "ip_address": unit_data.get("ip_address"),
            "phone_number": unit_data.get("phone_number"),
            "hardware_model": unit_data.get("hardware_model"),
        })

    # Add out-for-calibration units
    for unit_id, unit_data in snapshot["out_for_calibration"].items():
        units_list.append({
            "id": unit_id,
            "status": "Out for Calibration",
            "age": "N/A",
            "last_seen": "N/A",
            "deployed": False,
            "retired": False,
            "out_for_calibration": True,
            "ignored": False,
            "note": unit_data.get("note", ""),
            "device_type": unit_data.get("device_type", "seismograph"),
            "address": unit_data.get("address", ""),
            "coordinates": unit_data.get("coordinates", ""),
            "project_id": unit_data.get("project_id", ""),
            "last_calibrated": unit_data.get("last_calibrated"),
            "next_calibration_due": unit_data.get("next_calibration_due"),
            "deployed_with_modem_id": unit_data.get("deployed_with_modem_id"),
            "deployed_with_unit_id": unit_data.get("deployed_with_unit_id"),
            "ip_address": unit_data.get("ip_address"),
            "phone_number": unit_data.get("phone_number"),
            "hardware_model": unit_data.get("hardware_model"),
        })

    # Add retired units
    for unit_id, unit_data in snapshot["retired"].items():
        units_list.append({
            "id": unit_id,
            "status": "Retired",
            "age": "N/A",
            "last_seen": "N/A",
            "deployed": False,
            "retired": True,
            "out_for_calibration": False,
            "ignored": False,
            "note": unit_data.get("note", ""),
            "device_type": unit_data.get("device_type", "seismograph"),
            "address": unit_data.get("address", ""),
            "coordinates": unit_data.get("coordinates", ""),
            "project_id": unit_data.get("project_id", ""),
            "last_calibrated": unit_data.get("last_calibrated"),
            "next_calibration_due": unit_data.get("next_calibration_due"),
            "deployed_with_modem_id": unit_data.get("deployed_with_modem_id"),
            "deployed_with_unit_id": unit_data.get("deployed_with_unit_id"),
            "ip_address": unit_data.get("ip_address"),
            "phone_number": unit_data.get("phone_number"),
            "hardware_model": unit_data.get("hardware_model"),
        })

    # Add ignored units
    for unit_id, unit_data in snapshot.get("ignored", {}).items():
        units_list.append({
            "id": unit_id,
            "status": "Ignored",
            "age": "N/A",
            "last_seen": "N/A",
            "deployed": False,
            "retired": False,
            "out_for_calibration": False,
            "ignored": True,
            "note": unit_data.get("note", unit_data.get("reason", "")),
            "device_type": unit_data.get("device_type", "unknown"),
            "address": "",
            "coordinates": "",
            "project_id": "",
            "last_calibrated": None,
            "next_calibration_due": None,
            "deployed_with_modem_id": None,
            "deployed_with_unit_id": None,
            "ip_address": None,
            "phone_number": None,
            "hardware_model": None,
        })

    # Sort by status category, then by ID
    def sort_key(unit):
        # Priority: deployed (active) -> allocated -> benched -> out_for_calibration -> retired -> ignored
        if unit["deployed"]:
            return (0, unit["id"])
        elif unit.get("allocated"):
            return (1, unit["id"])
        elif not unit["retired"] and not unit["out_for_calibration"] and not unit["ignored"]:
            return (2, unit["id"])
        elif unit["out_for_calibration"]:
            return (3, unit["id"])
        elif unit["retired"]:
            return (4, unit["id"])
        else:
            return (5, unit["id"])

    units_list.sort(key=sort_key)

    return templates.TemplateResponse("partials/devices_table.html", {
        "request": request,
        "units": units_list,
        "timestamp": datetime.now().strftime("%H:%M:%S"),
        "user_timezone": get_user_timezone()
    })


@app.get("/health")
def health_check():
    """Health check endpoint"""
    return {
        "message": f"Seismo Fleet Manager v{VERSION}",
        "status": "running",
        "version": VERSION
    }


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8001)
