"""
CodeContext Server - Multi-Project Architecture

- ONE embedding model loaded at startup (shared)
- Separate database per project
- Lazy loading of project stores
"""

import os
import sys
import json
from pathlib import Path
from contextlib import asynccontextmanager
from typing import Dict

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

from .embedder import Embedder
from .gpu_utils import get_device, clear_gpu_memory
from .cli_utils import CODECONTEXT_ROOT, get_display_name
from .logging_config import configure_from_env, get_logger

# Initialize logging early (before other imports that might log)
configure_from_env()
logger = get_logger(__name__)

# Import services
from .services.store_manager import set_embedder
from .services.background import (
    set_config as set_background_config,
    start_periodic_consolidation,
    stop_periodic_consolidation,
    start_auto_resolve_loop,
    stop_auto_resolve_loop,
    process_existing_conflicts_background,
)

# Import route modules
from .routes import include_all_routers
from .utils.formatting import format_datetime


# Configuration
BASE_PATH = Path(os.environ.get(
    "CODECONTEXT_PATH",
    str(CODECONTEXT_ROOT)
))
DEFAULT_MODEL = os.environ.get("CODECONTEXT_MODEL", "all-MiniLM-L6-v2")
CONFIG_FILE = BASE_PATH / "config.json"

# Default config structure
DEFAULT_CONFIG = {
    "auto_resolve_enabled": False,
    "debug_mode": False,
    "auto_consolidation": {
        "enabled": False,
        "mode": "session",
        "periodic_minutes": 30,
        "threshold": 0.5
    },
    "summary": {
        "enabled": True,
        "auto_generate_on_session_end": True,
        "full_regen_every_n_sessions": 0,
        "full_regen_every_n_memories": 50,
        "max_tokens": 1000,
        "timeline_items": 10
    },
    "archive": {
        "enabled": True,
        "retention_days": 0,
        "auto_purge": False
    }
}


def load_config() -> Dict:
    """Load server configuration from disk."""
    if CONFIG_FILE.exists():
        try:
            config = json.loads(CONFIG_FILE.read_text())
            # Merge with defaults to ensure all keys exist
            for key, value in DEFAULT_CONFIG.items():
                if key not in config:
                    config[key] = value
                elif isinstance(value, dict):
                    for subkey, subvalue in value.items():
                        if subkey not in config[key]:
                            config[key][subkey] = subvalue
            return config
        except (json.JSONDecodeError, IOError):
            return DEFAULT_CONFIG.copy()
    return DEFAULT_CONFIG.copy()


def save_config(config: Dict) -> None:
    """Save server configuration to disk."""
    CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
    CONFIG_FILE.write_text(json.dumps(config, indent=2))


# Store the port for the ready message (set by __main__)
_server_port = 8765


# Lifespan: load model ONCE at startup
@asynccontextmanager
async def lifespan(app: FastAPI):
    logger.info("[Server] Starting up...")

    # Load server configuration
    server_config = load_config()
    auto_resolve_status = "ON" if server_config.get("auto_resolve_enabled") else "OFF"
    consolidation_settings = server_config.get("auto_consolidation", DEFAULT_CONFIG["auto_consolidation"])
    consolidation_status = "ON" if consolidation_settings.get("enabled") else "OFF"
    logger.info(f"[Server] Auto-resolve: {auto_resolve_status}")
    logger.info(f"[Server] Auto-consolidation: {consolidation_status} (mode={consolidation_settings.get('mode', 'session')})")
    logger.info(f"[Server] Base path: {BASE_PATH}")

    # Load embedding model ONCE
    device = get_device()
    model_name = DEFAULT_MODEL
    logger.info(f"[Server] Loading model: {model_name} on {device}...")
    logger.info("[Server] (this may take a few seconds on first run)")
    embedder = Embedder(model_name, device=device)
    logger.info(f"[Server] Model loaded! (dim={embedder.get_sentence_embedding_dimension()})")

    # Initialize services
    set_embedder(embedder, device)
    set_background_config(server_config)

    # Store state in app for route access
    app.state.embedder = embedder
    app.state.device = device
    app.state.model_name = model_name
    app.state.server_config = server_config
    app.state.base_path = BASE_PATH
    app.state.default_config = DEFAULT_CONFIG
    app.state.save_config = lambda cfg: save_config(cfg)
    app.state.process_existing_conflicts_background = process_existing_conflicts_background

    # Ensure base path exists
    BASE_PATH.mkdir(parents=True, exist_ok=True)

    # Print ready message with URL (use print for visibility in terminal)
    print("")
    print("=" * 50)
    print("  SERVER READY!")
    print(f"  Open: http://localhost:{_server_port}")
    print("=" * 50)
    print("")

    # Start periodic consolidation if enabled
    if consolidation_settings.get("enabled") and consolidation_settings.get("mode") in ("periodic", "both"):
        interval = consolidation_settings.get("periodic_minutes", 30)
        start_periodic_consolidation(interval)

    # Start auto-resolve background loop (always runs, checks toggle internally)
    start_auto_resolve_loop()

    yield

    # Stop background tasks on shutdown
    stop_periodic_consolidation()
    stop_auto_resolve_loop()
    logger.info("[Server] Shutting down...")
    clear_gpu_memory()


# Create app
app = FastAPI(title="CodeContext - Multi-Project", lifespan=lifespan)

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

# Register custom Jinja filters
templates.env.filters["format_datetime"] = format_datetime
templates.env.filters["display_name"] = get_display_name

# Store templates in app state for route access
app.state.templates = templates

# Include all route modules
include_all_routers(app)


# Run with: codecontext server
if __name__ == "__main__":
    import socket
    import uvicorn

    def is_port_available(port: int) -> bool:
        """Check if a port is available."""
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            try:
                s.bind(("0.0.0.0", port))
                return True
            except OSError:
                return False

    def find_available_port(start_port: int, max_attempts: int = 100) -> int:
        """Find an available port starting from start_port."""
        for i in range(max_attempts):
            port = start_port + i
            if is_port_available(port):
                return port
        raise RuntimeError(f"No available port found in range {start_port}-{start_port + max_attempts}")

    def get_pid_on_port(port: int) -> tuple | None:
        """Get PID and process name of process listening on port."""
        try:
            import psutil
            for conn in psutil.net_connections(kind='inet'):
                if conn.laddr.port == port and conn.status == 'LISTEN':
                    pid = conn.pid
                    if pid:
                        try:
                            process = psutil.Process(pid)
                            return (pid, process.name())
                        except psutil.NoSuchProcess:
                            return (pid, "unknown")
            return None
        except ImportError:
            # Fallback to Windows subprocess
            import subprocess
            import re
            try:
                result = subprocess.run(['netstat', '-ano'], capture_output=True, text=True, timeout=5)
                pattern = rf'^\s*TCP\s+\S*:{port}\s+\S*\s+LISTENING\s+(\d+)'
                for line in result.stdout.split('\n'):
                    match = re.search(pattern, line)
                    if match:
                        return (int(match.group(1)), "unknown")
            except Exception:
                pass
            return None
        except Exception:
            return None

    def kill_process_on_port(port: int) -> bool:
        """Kill any process listening on the specified port."""
        proc_info = get_pid_on_port(port)
        if not proc_info:
            return False

        pid, name = proc_info
        print(f"[Server] Killing existing process on port {port}: {name} (PID {pid})")

        try:
            import psutil
            process = psutil.Process(pid)
            process.kill()
            process.wait(timeout=5)
            return True
        except ImportError:
            # Fallback to Windows subprocess
            import subprocess
            try:
                subprocess.run(['taskkill', '/PID', str(pid), '/F'], capture_output=True, timeout=5)
                return True
            except Exception as e:
                print(f"[Server] Could not kill process: {e}")
                return False
        except Exception as e:
            print(f"[Server] Error killing process: {e}")
            return False

    # Get configured port (default 8765)
    configured_port = int(os.environ.get("CODECONTEXT_PORT", "8765"))

    # Use configured port only - auto-kill existing process if needed
    if is_port_available(configured_port):
        port = configured_port
    else:
        proc_info = get_pid_on_port(configured_port)
        if proc_info:
            pid, name = proc_info
            print(f"[Server] Port {configured_port} is busy (PID {pid}: {name}), attempting to kill...")
        else:
            print(f"[Server] Port {configured_port} is busy, attempting to kill existing process...")

        if kill_process_on_port(configured_port):
            import time
            time.sleep(1)  # Give OS time to release the port
            if is_port_available(configured_port):
                port = configured_port
                print(f"[Server] Port {configured_port} is now available")
            else:
                print(f"[Server] ERROR: Port {configured_port} still busy after kill attempt")
                if proc_info:
                    print(f"[Server] Manual kill: taskkill /F /PID {proc_info[0]}")
                sys.exit(1)
        else:
            print(f"[Server] ERROR: Could not kill process on port {configured_port}")
            if proc_info:
                print(f"[Server] Manual kill: taskkill /F /PID {proc_info[0]}")
            sys.exit(1)

    # Set port for the ready message in lifespan
    globals()['_server_port'] = port

    print("=" * 50)
    print("CodeContext Server - Multi-Project")
    print("=" * 50)
    print(f"Base path: {BASE_PATH}")
    print(f"Model: {DEFAULT_MODEL}")
    print(f"Port: {port}" + (f" (configured: {configured_port} was busy)" if port != configured_port else ""))
    print("")
    print("Starting server... please wait for READY message")
    print("=" * 50)

    uvicorn.run(app, host="0.0.0.0", port=port, workers=1)
