"""
GigLez FastAPI Application - Simplified Version

Runs without database requirement for testing web interface
"""

import csv
import io
import json
import sys
from pathlib import Path
from typing import List, Optional
from datetime import datetime
from fastapi import FastAPI, Request, UploadFile, File, Form
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.responses import HTMLResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

# Add project root to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent.parent))

from src.parser.sub_parser import SubFileParser
from src.parser.gps_extractor import GPSFilenameExtractor
from src.matcher.strategies import (
    ExactMatcher,
    FrequencyMatcher,
    PatternMatcher,
    TimingMatcher,
    RTL433DecoderStrategy,
    PatternBasedStrategy
)
from src.api.ingest import (
    detect_format,
    parse_rtl433,
    parse_csv,
    build_decoded_capture,
    dedup_key_for_file,
    dedup_key_for_capture,
    round_gps,
    iter_zip_members,
)

# =============================================================================
# APPLICATION INSTANCE
# =============================================================================

app = FastAPI(
    title="GigLez API",
    description="IoT RF Device Mapping Platform - Wigle for Sub-GHz Signals",
    version="1.0.0",
    docs_url="/docs",
    redoc_url="/redoc",
)

# Mount static files and templates
BASE_DIR = Path(__file__).parent.parent.parent
app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static")
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))

# Persistent storage file
STORAGE_FILE = BASE_DIR / "data" / "captures_simple.json"
STORAGE_FILE.parent.mkdir(exist_ok=True)

# In-memory storage for uploaded captures (simplified mode)
captures_storage = []
upload_counter = 0
# Dedup keys for everything already stored (rebuilt on load). A submission whose
# key is already present is skipped rather than stored twice.
seen_dedup_keys = set()

# Initialize unified matcher engine with all strategies
_matcher_engine = None

# Mock database class for simplified mode (no actual database)
class MockDB:
    """Mock database for simplified mode - strategies that need DB will gracefully fail"""
    def execute(self, query, params=None):
        return []

    def query(self, *args, **kwargs):
        return []

def get_matcher_engine():
    """Get or create unified matcher engine with all strategies"""
    global _matcher_engine
    if _matcher_engine is None:
        # Create mock database for strategies that need it
        mock_db = MockDB()

        # Initialize SignatureMatcher from engine.py
        from src.matcher.engine import SignatureMatcher
        _matcher_engine = SignatureMatcher(mock_db)

        # Add all strategies in order of confidence
        _matcher_engine.add_strategy(ExactMatcher())           # Exact protocol + frequency
        _matcher_engine.add_strategy(FrequencyMatcher())       # Frequency-based
        _matcher_engine.add_strategy(PatternMatcher())         # Bit pattern matching
        _matcher_engine.add_strategy(TimingMatcher())          # Timing-based
        _matcher_engine.add_strategy(RTL433DecoderStrategy())  # RTL_433 decoder
        _matcher_engine.add_strategy(PatternBasedStrategy())   # Pattern decoder (NEW!)
        print("✅ Initialized SignatureMatcher with 6 strategies (including Pattern Decoder)")
    return _matcher_engine


def load_captures():
    """Load captures from JSON file"""
    global captures_storage, upload_counter, seen_dedup_keys

    if STORAGE_FILE.exists():
        try:
            with open(STORAGE_FILE, 'r') as f:
                data = json.load(f)
                captures_storage = data.get('captures', [])
                upload_counter = data.get('upload_counter', 0)
                print(f"✅ Loaded {len(captures_storage)} captures from {STORAGE_FILE}")
        except Exception as e:
            print(f"⚠️  Failed to load captures: {e}")
            captures_storage = []
            upload_counter = 0
    else:
        print(f"ℹ️  No existing captures file at {STORAGE_FILE}")

    # Rebuild the dedup index so re-submitting the same file/observation is a no-op.
    seen_dedup_keys = set()
    for cap in captures_storage:
        key = cap.get("dedup_key") or dedup_key_for_capture(cap)
        seen_dedup_keys.add(key)


def save_captures():
    """Save captures to JSON file"""
    try:
        data = {
            'captures': captures_storage,
            'upload_counter': upload_counter,
            'last_updated': datetime.now().isoformat()
        }

        with open(STORAGE_FILE, 'w') as f:
            json.dump(data, f, indent=2)

    except Exception as e:
        print(f"⚠️  Failed to save captures: {e}")


# Load existing data on startup
load_captures()


# =============================================================================
# MIDDLEWARE
# =============================================================================

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

# GZip compression
app.add_middleware(GZipMiddleware, minimum_size=1000)


# =============================================================================
# ROOT ENDPOINTS
# =============================================================================

@app.get("/", response_class=HTMLResponse)
async def root(request: Request):
    """Root endpoint - Serve web interface"""
    return templates.TemplateResponse("index.html", {"request": request})


@app.get("/health")
async def health():
    """Health check endpoint"""
    return {
        "status": "healthy",
        "database": "not_connected",
        "mode": "simple"
    }


@app.get("/api")
async def api_root():
    """API information endpoint"""
    return {
        "name": "GigLez API",
        "version": "1.0.0",
        "description": "IoT RF Device Mapping Platform",
        "mode": "simple",
        "status": "operational"
    }


# =============================================================================
# MOCK API ENDPOINTS (for frontend testing)
# =============================================================================

@app.get("/api/v1/query/captures")
async def get_captures():
    """Return stored captures from in-memory storage"""
    return {
        "captures": captures_storage,
        "total": len(captures_storage),
        "page": 1,
        "page_size": 100
    }


@app.get("/api/v1/captures/{capture_id}")
async def get_capture_detail(capture_id: int):
    """Get detailed information for a specific capture"""
    # Find capture by ID
    capture = next((c for c in captures_storage if c.get("id") == capture_id), None)

    if not capture:
        return {
            "success": False,
            "message": f"Capture with ID {capture_id} not found"
        }

    return {
        "success": True,
        "capture": capture
    }


@app.get("/api/v1/stats/summary")
async def get_stats():
    """Return stats from in-memory storage"""
    # Calculate frequency distribution
    freq_dist = {}
    for capture in captures_storage:
        freq = capture.get("frequency", 0)
        if freq > 0:
            freq_dist[freq] = freq_dist.get(freq, 0) + 1

    # Count unique protocols as proxy for unique devices
    unique_protocols = len(set(c.get("protocol", "Unknown") for c in captures_storage))

    # Count by data source
    data_sources = {}
    for capture in captures_storage:
        source = capture.get("data_source", "unknown")
        data_sources[source] = data_sources.get(source, 0) + 1

    return {
        "total_captures": len(captures_storage),
        "unique_devices": unique_protocols,
        "coverage_area_km2": 0,
        "total_contributors": 1 if len(captures_storage) > 0 else 0,
        "frequency_distribution": freq_dist,
        "data_sources": data_sources,
        "captures_timeline": []
    }


# =============================================================================
# DATASET EXPORT (for model training)
# =============================================================================

# Columns exported for training datasets. Order is stable across formats.
EXPORT_FIELDS = [
    "id", "filename", "frequency", "protocol", "preset",
    "device_name", "device_category", "match_confidence", "match_method",
    "latitude", "longitude", "gps_source", "timestamp",
    "data_source", "session_id",
]


def _iter_export_rows(category: Optional[str], data_source: Optional[str]):
    """Yield capture dicts filtered by category / data_source."""
    for cap in captures_storage:
        if category and cap.get("device_category") != category:
            continue
        if data_source and cap.get("data_source") != data_source:
            continue
        yield cap


@app.get("/api/v1/export")
async def export_dataset(
    format: str = "jsonl",
    category: Optional[str] = None,
    data_source: Optional[str] = None,
):
    """
    Export captures as a labeled dataset for model training.

    Query params:
    - format:      jsonl (default) | csv | geojson
    - category:    filter to one device category (e.g. "Weather Sensor")
    - data_source: filter to one data source (e.g. "production")

    Each row carries the signal parameters + the auto-identified device
    label + category, suitable as training data for a Sub-GHz classifier.

    Note: this JSON-backed store does not retain raw pulse timing arrays;
    for RAW-pulse feature training, use the DB-backed path (Capture.raw_data).
    """
    fmt = format.lower()
    ts = datetime.now().strftime("%Y%m%d_%H%M%S")

    if fmt == "jsonl":
        def gen():
            for cap in _iter_export_rows(category, data_source):
                yield json.dumps({k: cap.get(k) for k in EXPORT_FIELDS}) + "\n"
        return StreamingResponse(
            gen(),
            media_type="application/x-ndjson",
            headers={"Content-Disposition": f'attachment; filename="giglez_dataset_{ts}.jsonl"'},
        )

    if fmt == "csv":
        def gen():
            buf = io.StringIO()
            writer = csv.DictWriter(buf, fieldnames=EXPORT_FIELDS, extrasaction="ignore")
            writer.writeheader()
            yield buf.getvalue()
            for cap in _iter_export_rows(category, data_source):
                buf.seek(0); buf.truncate(0)
                writer.writerow({k: cap.get(k) for k in EXPORT_FIELDS})
                yield buf.getvalue()
        return StreamingResponse(
            gen(),
            media_type="text/csv",
            headers={"Content-Disposition": f'attachment; filename="giglez_dataset_{ts}.csv"'},
        )

    if fmt == "geojson":
        features = []
        for cap in _iter_export_rows(category, data_source):
            lat, lon = cap.get("latitude"), cap.get("longitude")
            if lat is None or lon is None:
                continue
            features.append({
                "type": "Feature",
                "geometry": {"type": "Point", "coordinates": [lon, lat]},
                "properties": {k: cap.get(k) for k in EXPORT_FIELDS if k not in ("latitude", "longitude")},
            })
        return {"type": "FeatureCollection", "features": features}

    return {"error": f"Unsupported format '{format}'. Use jsonl, csv, or geojson."}


@app.delete("/api/v1/admin/cleanup")
async def cleanup_test_data(data_source: str = "test"):
    """
    Delete captures by data source (test, mock, or specific session)

    Query params:
    - data_source: The data source to delete (test, mock, production)
    """
    global captures_storage

    initial_count = len(captures_storage)

    # Filter out captures matching the data source
    captures_storage = [
        c for c in captures_storage
        if c.get("data_source", "production") != data_source
    ]

    deleted_count = initial_count - len(captures_storage)

    # Save updated data
    save_captures()

    return {
        "success": True,
        "message": f"Deleted {deleted_count} captures with data_source='{data_source}'",
        "deleted_count": deleted_count,
        "remaining_count": len(captures_storage)
    }


@app.delete("/api/v1/admin/cleanup/session/{session_id}")
async def cleanup_session(session_id: str):
    """Delete all captures from a specific session"""
    global captures_storage

    initial_count = len(captures_storage)

    # Filter out captures matching the session
    captures_storage = [
        c for c in captures_storage
        if c.get("session_id", "") != session_id
    ]

    deleted_count = initial_count - len(captures_storage)

    # Save updated data
    save_captures()

    return {
        "success": True,
        "message": f"Deleted {deleted_count} captures from session '{session_id}'",
        "deleted_count": deleted_count,
        "remaining_count": len(captures_storage)
    }


@app.delete("/api/v1/admin/cleanup/all")
async def cleanup_all_data():
    """Delete ALL captures (use with caution!)"""
    global captures_storage, upload_counter

    deleted_count = len(captures_storage)

    captures_storage = []
    upload_counter = 0

    # Save empty data
    save_captures()

    return {
        "success": True,
        "message": f"Deleted all {deleted_count} captures",
        "deleted_count": deleted_count,
        "remaining_count": 0
    }


@app.get("/api/rtl433/status")
async def rtl433_status():
    """Get RTL_433 decoder status"""
    try:
        decoder = get_rtl433_decoder()
        available = decoder._check_rtl433_available()
        version = decoder.get_version() if available else None

        return {
            "available": available,
            "version": version,
            "binary_path": decoder.rtl433_path,
            "timeout": decoder.timeout,
            "status": "operational" if available else "unavailable"
        }
    except Exception as e:
        return {
            "available": False,
            "version": None,
            "error": str(e),
            "status": "error"
        }


@app.get("/api/rtl433/protocols")
async def rtl433_protocols():
    """Get list of supported RTL_433 protocols"""
    try:
        decoder = get_rtl433_decoder()

        if not decoder._check_rtl433_available():
            return {
                "error": "RTL_433 is not available on this server",
                "total": 0,
                "protocols": []
            }

        protocols = decoder.get_supported_protocols()

        protocol_list = [
            {"id": pid, "name": name}
            for pid, name in sorted(protocols.items())
        ]

        return {
            "total": len(protocol_list),
            "protocols": protocol_list
        }

    except Exception as e:
        return {
            "error": f"Failed to retrieve protocols: {str(e)}",
            "total": 0,
            "protocols": []
        }


def _store_capture(cap: dict, dedup_key: str):
    """Assign an id and persist a capture, unless its dedup key already exists.

    Returns the stored capture dict, or None if it was a duplicate.
    """
    global upload_counter
    if dedup_key in seen_dedup_keys:
        return None
    upload_counter += 1
    cap["id"] = upload_counter
    cap["dedup_key"] = dedup_key
    seen_dedup_keys.add(dedup_key)
    captures_storage.append(cap)
    return cap


def _build_sub_capture(content: bytes, filename: str, entry: dict,
                       manifest_data: dict, gps_decimals):
    """Parse a Flipper .sub file, run the matcher, and build a capture record."""
    import tempfile
    import os

    sub_parser = SubFileParser()
    gps_extractor = GPSFilenameExtractor()

    temp_path = None
    try:
        with tempfile.NamedTemporaryFile(mode='wb', delete=False, suffix='.sub') as temp_file:
            temp_path = temp_file.name
            temp_file.write(content)
        metadata = sub_parser.parse(temp_path)
    finally:
        if temp_path and os.path.exists(temp_path):
            os.unlink(temp_path)

    gps_coords = gps_extractor.extract(filename)

    frequency = metadata.frequency if hasattr(metadata, 'frequency') else 0
    protocol = metadata.protocol if hasattr(metadata, 'protocol') else "RAW"
    preset = metadata.preset if hasattr(metadata, 'preset') else "Unknown"

    # Unified device matching (Exact, Frequency, BitPattern, Timing, RTL_433, Pattern Decoder)
    matcher_engine = get_matcher_engine()
    match_results = matcher_engine.match(metadata, max_results=10)
    best_match = match_results[0] if match_results else None
    best_category = (
        best_match.match_details.get("predicted_category") if best_match else None
    )

    lat = gps_coords.latitude if gps_coords else entry.get("latitude")
    lon = gps_coords.longitude if gps_coords else entry.get("longitude")
    lat, lon = round_gps(lat, lon, gps_decimals)

    return {
        "filename": filename,
        "frequency": frequency,
        "protocol": protocol,
        "preset": preset,
        "latitude": lat,
        "longitude": lon,
        "gps_source": gps_coords.source if gps_coords else "manual",
        "timestamp": entry.get("timestamp", ""),
        "data_source": manifest_data.get("data_source", "production"),
        "session_id": manifest_data.get("session_uuid", ""),
        "device_name": best_match.device_name if best_match else None,
        "device_category": best_category,
        "match_confidence": best_match.confidence if best_match else None,
        "match_method": best_match.match_method if best_match else None,
        "device_description": f"{best_match.manufacturer} {best_match.device_name}" if best_match else None,
        "matched_devices": [
            {
                "device_name": m.device_name,
                "manufacturer": m.manufacturer,
                # Per-device category is that device's own catalog category
                # (e.g. an Acurite sensor stays "Weather Sensor"). The overall
                # ML call lives in the top-level `device_category` field.
                "category": m.match_details.get("device_category")
                or m.match_details.get("predicted_category"),
                "confidence": m.confidence,
                "method": m.match_method,
                "details": m.match_details,
            }
            for m in match_results[:10]
        ] if match_results else [],
    }


def _ingest_one(filename: str, content: bytes, manifest_map: dict,
                manifest_data: dict, session_gps: dict, gps_decimals,
                successful: list, skipped: list, failed: list):
    """Detect a submission's format and route it to the right parser.

    Appends resulting capture records to successful / skipped / failed. ZIP
    batches recurse into their members.
    """
    fmt = detect_format(filename, content)
    entry = manifest_map.get(filename)
    if entry is None:
        entry = (manifest_data.get("captures") or [{}])[0]

    if fmt == "zip":
        for member_name, member_bytes in iter_zip_members(content):
            _ingest_one(member_name, member_bytes, manifest_map, manifest_data,
                        session_gps, gps_decimals, successful, skipped, failed)
        return

    if fmt == "sub":
        cap = _build_sub_capture(content, filename, entry, manifest_data, gps_decimals)
        stored = _store_capture(cap, dedup_key_for_file(content))
        (successful if stored else skipped).append(cap)
        return

    if fmt in ("rtl433_json", "csv"):
        records = parse_rtl433(content) if fmt == "rtl433_json" else parse_csv(content)
        if not records:
            failed.append({
                "filename": filename,
                "error": f"No usable records parsed from {fmt} file",
            })
            return
        for rec in records:
            cap = build_decoded_capture(
                rec,
                source_format=fmt,
                filename=filename,
                manifest_entry=entry,
                session_gps=session_gps,
                data_source=manifest_data.get("data_source", "production"),
                session_id=manifest_data.get("session_uuid", ""),
                gps_decimals=gps_decimals,
            )
            stored = _store_capture(cap, dedup_key_for_capture(cap))
            (successful if stored else skipped).append(cap)
        return

    failed.append({
        "filename": filename,
        "error": (f"Unsupported format for '{filename}'. Supported: Flipper .sub, "
                  f"rtl_433 .json/.ndjson, Wigle-style .csv, or a .zip batch."),
    })


@app.post("/api/v1/captures/upload")
async def upload_captures(
    files: List[UploadFile] = File(...),
    manifest: str = Form(...)
):
    """Ingest one or more captures.

    Accepts Flipper `.sub`, rtl_433 `.json`/`.ndjson`, Wigle-style `.csv`, and
    `.zip` batches (any mix of the above). Format is auto-detected per file.
    Duplicate submissions are skipped via a stable dedup key. Optional privacy
    GPS rounding is controlled by the manifest's `privacy_gps_decimals`.
    """
    try:
        manifest_data = json.loads(manifest)

        # Per-file manifest entries carry each file's GPS/timestamp.
        manifest_map = {
            c.get("filename"): c
            for c in manifest_data.get("captures", [])
        }
        # Session-level GPS fallback (used by decoded records without their own coords).
        session_gps = {
            "latitude": manifest_data.get("latitude"),
            "longitude": manifest_data.get("longitude"),
        }
        gps_decimals = manifest_data.get("privacy_gps_decimals")

        successful, skipped, failed = [], [], []

        for file in files:
            try:
                content = await file.read()
                _ingest_one(file.filename, content, manifest_map, manifest_data,
                            session_gps, gps_decimals, successful, skipped, failed)
            except Exception as e:
                failed.append({"filename": file.filename, "error": str(e)})

        save_captures()

        return {
            "success": True,
            "message": (f"Processed {len(successful)} records "
                        f"({len(skipped)} duplicates skipped, {len(failed)} failed)"),
            "successful": successful,
            "skipped": skipped,
            "failed": failed,
            "total_uploaded": len(files),
            "total_successful": len(successful),
            "total_skipped": len(skipped),
            "total_failed": len(failed),
        }

    except Exception as e:
        return {
            "success": False,
            "message": f"Upload failed: {str(e)}",
            "successful": [],
            "skipped": [],
            "failed": [],
            "total_uploaded": 0,
            "total_successful": 0,
            "total_skipped": 0,
            "total_failed": 0,
        }


# =============================================================================
# RUN WITH UVICORN (for development)
# =============================================================================

if __name__ == "__main__":
    import uvicorn

    print("=" * 80)
    print("GigLez Web Interface - Simple Mode")
    print("=" * 80)
    print()
    print("Starting server...")
    print()
    print("Web Interface: http://localhost:8000")
    print("API Docs:      http://localhost:8000/docs")
    print("Health Check:  http://localhost:8000/health")
    print()
    print("NOTE: This is a simplified version for testing the web interface.")
    print("      Upload and database features are not available.")
    print()
    print("Press Ctrl+C to stop")
    print("=" * 80)
    print()

    uvicorn.run(
        app,
        host="0.0.0.0",
        port=8000,
        reload=False,
        log_level="info",
    )
