#!/usr/bin/env python3
"""
FastAPI main application for GNSS Guard Server
Centralized monitoring server for multiple GNSS Guard assets
"""

import asyncio
import logging
import json
import random
from contextlib import asynccontextmanager
from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional

from fastapi import FastAPI, Request, Depends, HTTPException
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded

from config import get_config
from database import init_db, get_db, get_session_factory
from routes import api, auth
from routes.auth import get_optional_user, get_current_user
from services.asset_service import AssetService
from services.telegram_service import get_telegram_service
from models import Asset, AssetNotificationState

# Initialize rate limiter
limiter = Limiter(key_func=get_remote_address)

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

# Create FastAPI app
app = FastAPI(
    title="GNSS Guard Server",
    description="Centralized monitoring server for GNSS Guard assets",
    version="1.0.0"
)

# Setup rate limiting
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

# Add CORS middleware - restricted to same-origin only
# Since the dashboard is served from the same domain, we only need
# to allow requests from the same origin. This prevents CSRF attacks.
config = get_config()
allowed_origins = []
if config.server_domain:
    allowed_origins = [
        f"https://{config.server_domain}",
        f"http://{config.server_domain}",  # For initial setup before SSL
    ]

app.add_middleware(
    CORSMiddleware,
    allow_origins=allowed_origins,
    allow_credentials=True,
    allow_methods=["GET", "POST", "DELETE"],
    allow_headers=["Content-Type", "Authorization", "Cookie"],
)

# Setup static files and templates
static_path = Path(__file__).parent / "static"
templates_path = Path(__file__).parent / "templates"

if static_path.exists():
    app.mount("/static", StaticFiles(directory=str(static_path)), name="static")

templates = Jinja2Templates(directory=str(templates_path)) if templates_path.exists() else None

# Include routers
app.include_router(api.router)
app.include_router(auth.router)


# =============================================================================
# Health Check Endpoint (public, no auth required)
# =============================================================================

@app.get("/health")
async def health_check():
    """Health check endpoint - always accessible"""
    return {"status": "ok", "timestamp": datetime.utcnow().isoformat()}


async def check_offline_assets():
    """Background task to check for assets that have gone offline"""
    config = get_config()
    telegram_service = get_telegram_service()
    
    if not telegram_service.enabled:
        return
    
    threshold = datetime.utcnow() - timedelta(seconds=config.asset_offline_seconds)
    
    SessionLocal = get_session_factory()
    db = SessionLocal()
    try:
        # Find assets that are marked online but haven't reported recently
        states = db.query(AssetNotificationState).join(Asset).filter(
            AssetNotificationState.is_online == True,
            AssetNotificationState.last_validation_at != None,
            AssetNotificationState.last_validation_at < threshold,
            Asset.is_active == True,
            Asset.telegram_enabled == True
        ).all()
        
        for state in states:
            chat_id = state.asset.telegram_chat_id or telegram_service.default_chat_id
            if chat_id:
                logger.info(f"Asset '{state.asset.name}' detected as offline (last seen: {state.last_validation_at})")
                telegram_service.send_asset_offline_alert(
                    chat_id=chat_id,
                    asset_name=state.asset.name,
                    last_seen=state.last_validation_at,
                    offline_threshold_seconds=config.asset_offline_seconds
                )
                state.is_online = False
        
        if states:
            db.commit()
            
    except Exception as e:
        logger.error(f"Error checking offline assets: {e}")
        db.rollback()
    finally:
        db.close()


async def offline_checker_loop():
    """Background loop that periodically checks for offline assets"""
    while True:
        await asyncio.sleep(30)  # Check every 30 seconds
        try:
            await check_offline_assets()
        except Exception as e:
            logger.error(f"Error in offline checker loop: {e}")


@app.on_event("startup")
async def startup_event():
    """Initialize database and background tasks on startup"""
    logger.info("Starting GNSS Guard Server...")
    init_db()
    logger.info("Database initialized")
    
    # Start background task for offline detection
    asyncio.create_task(offline_checker_loop())
    logger.info("Offline asset checker started")


# =============================================================================
# Web UI Routes
# =============================================================================

@app.get("/", response_class=HTMLResponse)
async def index(request: Request, user: Optional[str] = Depends(get_optional_user)):
    """Main dashboard page"""
    if not user:
        return RedirectResponse(url="/login", status_code=302)
    
    if not templates:
        return HTMLResponse("<h1>GNSS Guard Server</h1><p>Templates not configured</p>")
    
    return templates.TemplateResponse("dashboard.html", {
        "request": request,
        "username": user,
        "cache_buster": random.randint(100000, 999999)
    })


@app.get("/login", response_class=HTMLResponse)
async def login_page(request: Request, user: Optional[str] = Depends(get_optional_user)):
    """Login page"""
    if user:
        return RedirectResponse(url="/", status_code=302)
    
    if not templates:
        return HTMLResponse("""
        <h1>GNSS Guard Server - Login</h1>
        <form method="post" action="/login">
            <input name="username" placeholder="Username"><br>
            <input name="password" type="password" placeholder="Password"><br>
            <button type="submit">Login</button>
        </form>
        """)
    
    return templates.TemplateResponse("login.html", {
        "request": request,
        "cache_buster": random.randint(100000, 999999)
    })


@app.get("/api/dashboard/assets")
async def dashboard_assets(
    user: str = Depends(get_current_user),
    db: Session = Depends(get_db)
):
    """Get all assets status for dashboard"""
    service = AssetService(db)
    return service.get_all_assets_status()


@app.get("/api/dashboard/asset/{asset_name}/status")
async def dashboard_asset_status(
    asset_name: str,
    at: Optional[float] = None,
    user: str = Depends(get_current_user),
    db: Session = Depends(get_db)
):
    """
    Get detailed status for a specific asset (for dashboard display).
    Matches the format expected by the client dashboard.
    
    Args:
        at: Optional Unix timestamp to get historical data at that time.
            If not provided, returns the latest data.
    """
    service = AssetService(db)
    asset = service.get_asset_by_name(asset_name)
    
    if not asset:
        raise HTTPException(status_code=404, detail=f"Asset '{asset_name}' not found")
    
    if at is not None:
        # Get historical validation at specified timestamp
        latest = service.get_validation_at_timestamp(asset.id, at)
    else:
        latest = service.get_latest_validation(asset.id)
    
    if not latest:
        return {
            "error": "No validation data available",
            "timestamp": datetime.utcnow().isoformat()
        }
    
    # Parse JSON fields
    sources_missing = json.loads(latest.sources_missing or "[]")
    sources_stale = json.loads(latest.sources_stale or "[]")
    coordinate_differences = json.loads(latest.coordinate_differences or "{}")
    source_coordinates = json.loads(latest.source_coordinates or "{}")
    validation_details = json.loads(latest.validation_details or "{}")
    
    # Get enabled sources from validation_details
    expected_sources = validation_details.get("expected_sources", [])
    
    # Build sources status (matching client format)
    source_display_names = {
        "nmea_primary": "Primary GPS",
        "nmea_secondary": "Secondary GPS",
        "tm_ais": "TM AIS GPS",
        "starlink_gps": "Starlink GPS",
        "starlink_location": "Starlink Location"
    }
    
    sources = {}
    all_source_names = ["nmea_primary", "nmea_secondary", "tm_ais", "starlink_gps", "starlink_location"]
    
    for source_name in all_source_names:
        display_name = source_display_names.get(source_name, source_name)
        
        if source_name not in expected_sources:
            sources[source_name] = {
                "display_name": display_name,
                "enabled": False,
                "status": "not_configured",
                "is_stale": False,
                "coordinates": None,
                "last_update": None,
                "last_update_unix": None
            }
            continue
        
        source_data = source_coordinates.get(source_name)
        is_stale = source_name in sources_stale
        
        if not source_data:
            sources[source_name] = {
                "display_name": display_name,
                "enabled": True,
                "status": "missing",
                "is_stale": is_stale,
                "coordinates": None,
                "last_update": None,
                "last_update_unix": None
            }
        else:
            status = "stale" if is_stale else "ok"
            sources[source_name] = {
                "display_name": display_name,
                "enabled": True,
                "status": status,
                "is_stale": is_stale,
                "coordinates": {
                    "latitude": source_data.get("latitude"),
                    "longitude": source_data.get("longitude")
                },
                "last_update": source_data.get("timestamp"),
                "last_update_unix": source_data.get("timestamp_unix")
            }
    
    # Calculate max distance
    threshold_meters = validation_details.get("threshold_meters", 200.0)
    max_distance_km = None
    max_distance_m = 0.0
    
    if not latest.is_valid and coordinate_differences:
        for diff_data in coordinate_differences.values():
            if isinstance(diff_data, dict):
                distance = diff_data.get("distance_meters", diff_data.get("distance_m", 0))
                if distance > max_distance_m:
                    max_distance_m = distance
        
        if max_distance_m > threshold_meters:
            max_distance_km = max_distance_m / 1000.0
    
    has_alert = (not latest.is_valid and max_distance_km is not None) or len(sources_missing) > 0
    
    # Find map center
    map_center = None
    for priority_source in ["nmea_primary", "tm_ais", "starlink_location"]:
        if sources.get(priority_source, {}).get("coordinates"):
            coords = sources[priority_source]["coordinates"]
            if coords.get("latitude") and coords.get("longitude"):
                map_center = coords
                break
    
    if not map_center:
        for source_data in sources.values():
            if source_data.get("coordinates"):
                coords = source_data["coordinates"]
                if coords.get("latitude") and coords.get("longitude"):
                    map_center = coords
                    break
    
    return {
        "timestamp": datetime.utcnow().isoformat(),
        "validation_timestamp": latest.validation_timestamp,
        "validation_timestamp_unix": latest.validation_timestamp_unix,
        "is_valid": latest.is_valid,
        "has_alert": has_alert,
        "max_distance_km": max_distance_km,
        "threshold_meters": threshold_meters,
        "sources": sources,
        "sources_stale": sources_stale,
        "map_center": map_center,
        "asset_name": asset_name
    }


@app.get("/api/dashboard/asset/{asset_name}/route")
async def dashboard_asset_route(
    asset_name: str,
    hours: int = 72,
    until: Optional[float] = None,
    user: str = Depends(get_current_user),
    db: Session = Depends(get_db)
):
    """
    Get route data for map visualization.
    
    Args:
        hours: Number of hours of history (default 72)
        until: Optional Unix timestamp to show route up to this time.
               If not provided, shows route up to current time.
    """
    service = AssetService(db)
    asset = service.get_asset_by_name(asset_name)
    
    if not asset:
        raise HTTPException(status_code=404, detail=f"Asset '{asset_name}' not found")
    
    return service.get_route_data(asset.id, hours, until_timestamp=until)


# =============================================================================
# Main entry point
# =============================================================================

def run_server():
    """Run the server using uvicorn"""
    import uvicorn
    config = get_config()
    
    uvicorn.run(
        "server.main:app",
        host=config.server_host,
        port=config.server_port,
        reload=config.debug,
        log_level="info"
    )


if __name__ == "__main__":
    run_server()

