"""
================================================================================
Quantum Terminal Consumer — Launcher
================================================================================
Single entry point for the packaged .exe.
Starts data server + serves React frontend + opens browser.
Auto-creates consumer_config.ini on first run.

Usage:
    python launcher.py              # Dev mode
    QuantumTerminal.exe                    # Packaged mode

This module does NOT introduce any calculation triggers.
================================================================================
"""

import sys
import os
import threading
import time
import webbrowser
import shutil
import logging
from pathlib import Path

log = logging.getLogger("mk.launcher")

# — Determine base paths (works for both dev and PyInstaller) —
if getattr(sys, 'frozen', False):
    # Running as packaged .exe — everything is flat in _MEIPASS
    BASE_DIR = Path(sys._MEIPASS)
    EXE_DIR = Path(sys.executable).parent
    BACKEND_DIR = BASE_DIR          # PyInstaller bundles flat, no backend/ subfolder
    FRONTEND_DIR = BASE_DIR / "frontend"
else:
    # Running as script (dev mode)
    BASE_DIR = Path(__file__).resolve().parent
    EXE_DIR = BASE_DIR
    BACKEND_DIR = BASE_DIR
    FRONTEND_DIR = BASE_DIR.parent / "terminal_app" / "build"

PORT = int(os.environ.get("MK_PORT", "8502"))   # v2: Electron passes MK_PORT

# v2: AppData dir name (env-var driven; default "QuantumTerminal" preserves v1 path).
APP_DIR = os.environ.get("MK_APP_DIR_NAME", "QuantumTerminal")

# — Default config content (f-string so APP_DIR is substituted at module load) —
DEFAULT_CONFIG = f"""\
[server]
# No external server needed for Quantum Terminal (open source)
base_url = 

[data]
cache_dir = %APPDATA%\\{APP_DIR}\\cache
stale_warning_hours = 48
stale_error_hours = 72
"""


def ensure_config():
    """Create consumer_config.ini on first run. User never touches this."""
    # Check next to .exe first (packaged mode)
    config_path = EXE_DIR / "consumer_config.ini"

    if config_path.exists():
        return config_path

    # Also check AppData (fallback location)
    appdata_config = _get_appdata_base() / "consumer_config.ini"
    if appdata_config.exists():
        return appdata_config

    # Try to write next to .exe
    try:
        template = BASE_DIR / "consumer_config.ini.template"
        if template.exists():
            shutil.copy2(template, config_path)
        else:
            config_path.write_text(DEFAULT_CONFIG, encoding="utf-8")
        print(f"  Config created: {config_path}")
        return config_path
    except PermissionError:
        # Program Files is write-protected — use AppData instead
        appdata_dir = _get_appdata_base()
        appdata_dir.mkdir(parents=True, exist_ok=True)
        appdata_config = appdata_dir / "consumer_config.ini"
        try:
            template = BASE_DIR / "consumer_config.ini.template"
            if template.exists():
                shutil.copy2(template, appdata_config)
            else:
                appdata_config.write_text(DEFAULT_CONFIG, encoding="utf-8")
            print(f"  Config created (AppData): {appdata_config}")
            return appdata_config
        except Exception as e:
            print(f"  [WARN] Could not create config: {e}")
            return appdata_config


def _get_appdata_base() -> Path:
    """Get AppData\\Roaming\\<APP_DIR> path. APP_DIR comes from MK_APP_DIR_NAME env var."""
    import platform
    if platform.system() == "Windows":
        appdata = os.environ.get("APPDATA", "")
        if appdata:
            return Path(appdata) / APP_DIR
    return Path.home() / f".{APP_DIR.lower()}"


def ensure_appdata():
    """Create AppData directories for auth cache and data cache."""
    appdata = os.environ.get("APPDATA", "")
    if appdata:
        base = Path(appdata) / APP_DIR
    else:
        base = Path.home() / "AppData" / "Roaming" / APP_DIR

    for subdir in ["", "cache"]:
        d = base / subdir if subdir else base
        d.mkdir(parents=True, exist_ok=True)

    return base


def open_browser():
    """Wait for server to start, then open browser."""
    for _ in range(30):  # Wait up to 30 seconds
        time.sleep(1)
        try:
            import urllib.request
            urllib.request.urlopen(f"http://127.0.0.1:{PORT}/api/health", timeout=2)
            break
        except Exception:
            continue

    webbrowser.open(f"http://127.0.0.1:{PORT}")


def main():
    # v2 Phase D: persistent rotating log file under %APPDATA%\<APP_DIR>\logs\.
    #   Console behavior unchanged; file logging is additive.
    try:
        from logging_setup import setup_logging
        _log_file = setup_logging()
    except Exception as _e:
        _log_file = None
        print(f"[launcher] logging setup skipped: {_e}")

    print()
    print("=" * 50)
    print("  Quantum Terminal Consumer Terminal")
    print("=" * 50)
    if _log_file:
        print(f"  Logs   : {_log_file}")

    # Step 1: Ensure config exists
    config_path = ensure_config()
    print(f"  Config : {config_path}")

    # Step 2: Ensure AppData dirs exist
    appdata = ensure_appdata()
    print(f"  AppData: {appdata}")

    # Step 3: Setup Python path
    sys.path.insert(0, str(BACKEND_DIR))
    os.chdir(str(BACKEND_DIR))

    # Also add EXE_DIR so consumer_config.ini is found
    if str(EXE_DIR) not in sys.path:
        sys.path.insert(0, str(EXE_DIR))

    # Add config directory to path so modules find it
    config_dir = str(config_path.parent)
    if config_dir not in sys.path:
        sys.path.insert(0, config_dir)

    # Copy config next to backend modules if packaged
    if getattr(sys, 'frozen', False):
        backend_config = BACKEND_DIR / "consumer_config.ini"
        if not backend_config.exists() and config_path.exists():
            try:
                shutil.copy2(config_path, backend_config)
            except PermissionError:
                pass  # Read-only _internal — config found via sys.path instead

    print(f"  Server : http://127.0.0.1:{PORT}")
    print(f"  Press Ctrl+C to stop")
    print("=" * 50)
    print()

    # Step 4: Open browser in background — skipped when running under Electron.
    if not os.environ.get("MK_NO_BROWSER"):
        threading.Thread(target=open_browser, daemon=True).start()

    # Step 5: Import and start server
    from data_server import app

    # Serve React build as static files (production mode)
    # IMPORTANT: Use StaticFiles mount (not catch-all routes) so API routes
    # always take priority. The catch-all @app.get("/{path}") pattern causes
    # 405 errors on PATCH/POST to /api/* endpoints.
    if FRONTEND_DIR.exists() and (FRONTEND_DIR / "index.html").exists():
        from fastapi.staticfiles import StaticFiles
        from fastapi.responses import FileResponse
        from starlette.middleware.base import BaseHTTPMiddleware
        from starlette.responses import Response as StarletteResponse

        # Helper: serve index.html with no-cache headers so browser
        # always fetches the latest after an update. JS/CSS files have
        # content hashes in their filenames so they cache safely.
        def _serve_index():
            return FileResponse(
                str(FRONTEND_DIR / "index.html"),
                headers={
                    "Cache-Control": "no-cache, no-store, must-revalidate",
                    "Pragma": "no-cache",
                    "Expires": "0",
                },
            )

        # Serve /static/* assets (JS, CSS, images)
        app.mount("/static", StaticFiles(directory=str(FRONTEND_DIR / "static")), name="static")

        # SPA middleware: for non-API, non-static GET requests that don't match
        # a file, serve index.html. This replaces the old @app.get("/{full_path:path}")
        # catch-all which caused 405 errors on API PATCH/POST requests.
        class SPAMiddleware(BaseHTTPMiddleware):
            async def dispatch(self, request, call_next):
                response = await call_next(request)

                # If the API returned 404 and this is a GET request
                # that doesn't target /api/ or /ws/, serve index.html
                # (React client-side routing)
                if (
                    response.status_code == 404
                    and request.method == "GET"
                    and not request.url.path.startswith("/api/")
                    and not request.url.path.startswith("/ws/")
                    and not request.url.path.startswith("/static/")
                ):
                    # Check if it's a real file in the build directory
                    file_path = FRONTEND_DIR / request.url.path.lstrip("/")
                    if file_path.exists() and file_path.is_file():
                        return FileResponse(str(file_path))
                    # Otherwise serve index.html for React Router
                    return _serve_index()

                return response

        app.add_middleware(SPAMiddleware)

        # Root route — serve index.html
        @app.get("/", include_in_schema=False)
        async def serve_root():
            return _serve_index()

        # Favicon
        favicon_path = FRONTEND_DIR / "favicon.ico"
        if favicon_path.exists():
            @app.get("/favicon.ico", include_in_schema=False)
            async def serve_favicon():
                return FileResponse(str(favicon_path))

        print("  Frontend: serving React build")
    else:
        print("  Frontend: not found — use npm start for dev mode")

    # v2 Phase E: graceful shutdown.
    #   Electron sends SIGTERM (Windows: terminates the process group) when
    #   the user closes the window. uvicorn handles SIGTERM by setting its
    #   `should_exit` flag and draining in-flight requests. We additionally
    #   register a Python-level signal handler that flushes logs and lets
    #   any pending sync_timestamps write finish before exit. SIGINT (Ctrl+C
    #   in dev) goes through the same path.
    import uvicorn
    import signal

    config = uvicorn.Config(
        app=app,
        host="127.0.0.1",
        port=PORT,
        log_level="info",
        # Give in-flight HTTP requests up to 5s to finish before forcing exit.
        timeout_graceful_shutdown=5,
    )
    server = uvicorn.Server(config)

    def _on_signal(signum, frame):
        log.info(f"Received signal {signum} — initiating graceful shutdown")
        server.should_exit = True

    for sig in (signal.SIGINT, signal.SIGTERM):
        try:
            signal.signal(sig, _on_signal)
        except Exception:
            # Some platforms don't allow custom handlers in non-main threads.
            pass

    try:
        server.run()
    finally:
        # Final flush — make sure log records on disk are complete and
        # any in-memory state (sync timestamps) has been persisted.
        try:
            from data_sync_client import get_sync_client as _gsc
            _sc = _gsc()
            try:
                # Best-effort: persist sync timestamps if anything was changed
                # mid-request when the signal arrived.
                from data_sync_client import _save_sync_timestamps
                _save_sync_timestamps(_sc._sync_timestamps)
            except Exception:
                pass
        except Exception:
            pass
        try:
            logging.shutdown()
        except Exception:
            pass


if __name__ == "__main__":
    main()