from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field, field_validator
import json
import logging
import os
import sys
from pathlib import Path
from typing import Optional

from app.database import SessionDep

router = APIRouter(prefix="/api/settings", tags=["settings"])
logger = logging.getLogger(__name__)

# In frozen PyInstaller builds, write to %APPDATA%\CerebionRivet\ (writable).
# In development, write to data/ next to the app package.
if getattr(sys, "frozen", False):
    _appdata = os.environ.get("APPDATA") or str(Path.home())
    _data_dir = Path(_appdata) / "CerebionRivet"
else:
    _data_dir = Path(__file__).parent.parent.parent / "data"  # rivet_server/data/
_data_dir.mkdir(parents=True, exist_ok=True)
SETTINGS_FILE = _data_dir / "settings.json"

class BinaryAnalyzerSettings(BaseModel):
    max_binary_size_mb: int = Field(default=500, ge=1, le=5000, description="Maximum binary file size in MB")
    analysis_timeout_seconds: int = Field(default=600, ge=30, le=3600, description="Analysis timeout in seconds")
    max_memory_mb: int = Field(default=2048, ge=512, le=8192, description="Maximum memory usage in MB")
    max_concurrent_scans: int = Field(default=3, ge=1, le=10, description="Maximum concurrent scans")
    max_archive_size_mb: int = Field(default=500, ge=1, le=2000, description="Maximum archive size in MB")
    max_files_per_archive: int = Field(default=100, ge=1, le=1000, description="Maximum files to extract per archive")
    analysis_backend: str = Field(default="lief", description="Analysis backend: 'lief' or 'ghidra'")

DEFAULT_NETWORK_PORTS = [443, 465, 587, 636, 993, 995, 5671, 8443]

class NetworkAnalyzerSettings(BaseModel):
    ports: list[int] = Field(default_factory=lambda: DEFAULT_NETWORK_PORTS, description="Ports to scan")
    max_bulk_targets: int = Field(default=50, ge=1, le=500, description="Maximum targets in a bulk scan")

    @field_validator("ports")
    @classmethod
    def validate_ports(cls, v):
        """Mirror PortScanRequest's port validation (scan.py) -- without this,
        an invalid default port list saves successfully here but then fails
        every subsequent scan that uses it, with no link back to the bad
        saved setting as the cause."""
        if len(v) > 100:
            raise ValueError("Too many ports. Maximum 100 ports.")
        invalid = [p for p in v if p < 1 or p > 65535]
        if invalid:
            raise ValueError(
                f"Invalid port numbers: {invalid}. Ports must be between 1 and 65535."
            )
        return v

class LicenseSettings(BaseModel):
    license_server_url: str = Field(default="https://license.cerebion.com", description="License server URL")
    offline_public_key: str = Field(default="", description="Ed25519 public key for offline license verification")

class AppSettings(BaseModel):
    binary_analyzer: BinaryAnalyzerSettings = Field(default_factory=BinaryAnalyzerSettings)
    network_analyzer: NetworkAnalyzerSettings = Field(default_factory=NetworkAnalyzerSettings)
    license: LicenseSettings = Field(default_factory=LicenseSettings)
    last_folder_path: Optional[str] = None

def load_settings() -> AppSettings:
    """Load settings from file or return defaults"""
    if SETTINGS_FILE.exists():
        try:
            data = json.loads(SETTINGS_FILE.read_text())
            return AppSettings(**data)
        except Exception as e:
            # A parse failure here silently resets ALL settings to defaults on
            # the next load with no user-facing indication -- at minimum this
            # needs to go through real logging (so it shows up wherever the
            # app's other errors do) instead of print(), which is easy to miss.
            logger.error(f"Failed to parse {SETTINGS_FILE}, resetting to defaults: {e}")
    return AppSettings()

def save_settings(settings: AppSettings):
    """Save settings to file.

    Writes to a temp file and atomically replaces the real file (os.replace
    is atomic on both Windows and POSIX) rather than writing in place, so a
    crash or concurrent write mid-save can't leave a truncated/corrupted
    settings.json that load_settings() would then silently treat as "reset
    everything to defaults".
    """
    SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True)
    tmp_file = SETTINGS_FILE.with_suffix(".json.tmp")
    tmp_file.write_text(json.dumps(settings.dict(), indent=2))
    os.replace(tmp_file, SETTINGS_FILE)

@router.get("/binary-analyzer")
def get_binary_analyzer_settings():
    """Get binary analyzer settings with Ghidra availability"""
    settings = load_settings()
    
    # Check if Ghidra is available
    ghidra_available = False
    try:
        from common.ghidra import is_available as ghidra_is_available
        ghidra_available = ghidra_is_available()
    except:
        pass
    
    result = settings.binary_analyzer.dict()
    result["ghidra_available"] = ghidra_available
    return result

@router.put("/binary-analyzer")
def update_binary_analyzer_settings(new_settings: BinaryAnalyzerSettings):
    """Update binary analyzer settings"""
    settings = load_settings()
    settings.binary_analyzer = new_settings
    save_settings(settings)
    return {"status": "success", "settings": new_settings.dict()}

@router.get("/network-analyzer")
def get_network_analyzer_settings():
    return load_settings().network_analyzer.dict()

@router.put("/network-analyzer")
def update_network_analyzer_settings(new_settings: NetworkAnalyzerSettings):
    settings = load_settings()
    settings.network_analyzer = new_settings
    save_settings(settings)
    return {"status": "success", "settings": new_settings.dict()}

@router.get("/license")
def get_license_settings():
    return load_settings().license.dict()

@router.put("/license")
def update_license_settings(new_settings: LicenseSettings):
    settings = load_settings()
    settings.license = new_settings
    save_settings(settings)
    return {"status": "success", "settings": new_settings.dict()}

@router.get("/workspace")
def get_workspace():
    return {"last_folder_path": load_settings().last_folder_path}

@router.put("/workspace")
def set_workspace(body: dict):
    settings = load_settings()
    settings.last_folder_path = body.get("last_folder_path")
    save_settings(settings)
    return {"last_folder_path": settings.last_folder_path}

@router.get("/all")
def get_all_settings():
    """Get all application settings"""
    return load_settings().dict()

@router.delete("/scan-cache")
def clear_scan_cache(project_root: Optional[str] = None, session: SessionDep = None):
    """Clear the incremental scan cache.

    Pass ?project_root=<path> to clear only a specific project's cache.
    Omit to clear all cached results.
    """
    from app.code_analyzer.scan_cache import ScanCache
    cache = ScanCache(session)
    if project_root:
        count = cache.clear_project(project_root)
        return {"success": True, "entries_cleared": count, "scope": project_root}
    count = cache.clear_all()
    return {"success": True, "entries_cleared": count, "scope": "all"}


@router.get("/scan-cache/stats")
def get_scan_cache_stats(project_root: Optional[str] = None, session: SessionDep = None):
    """Return cache statistics (entry count, languages, oldest/newest entry)."""
    from app.code_analyzer.scan_cache import ScanCache
    cache = ScanCache(session)
    return cache.get_stats(project_root)


# Global settings accessor
def get_binary_analyzer_limits():
    """Get current binary analyzer limits for use in analyzers"""
    settings = load_settings()
    return {
        "max_binary_size": settings.binary_analyzer.max_binary_size_mb * 1_000_000,
        "analysis_timeout": settings.binary_analyzer.analysis_timeout_seconds,
        "max_memory_mb": settings.binary_analyzer.max_memory_mb,
        "max_concurrent_scans": settings.binary_analyzer.max_concurrent_scans,
        "max_archive_size": settings.binary_analyzer.max_archive_size_mb * 1_000_000,
        "max_files_per_archive": settings.binary_analyzer.max_files_per_archive,
        "analysis_backend": settings.binary_analyzer.analysis_backend
    }

_VALID_LOG_LEVELS = {"DEBUG", "INFO", "WARNING", "ERROR"}

class LogLevelRequest(BaseModel):
    level: str = Field(description="Log level: DEBUG, INFO, WARNING, or ERROR")

@router.get("/log-level")
def get_log_level():
    """Get the current root log level."""
    return {"level": logging.getLevelName(logging.getLogger().level)}

@router.put("/log-level")
def set_log_level(body: LogLevelRequest):
    """Set the root log level at runtime (takes effect immediately, no restart needed)."""
    level = body.level.upper()
    if level not in _VALID_LOG_LEVELS:
        raise HTTPException(status_code=422, detail=f"Invalid log level. Must be one of: {', '.join(sorted(_VALID_LOG_LEVELS))}")
    logging.getLogger().setLevel(level)
    # Also update all file handlers so they filter at the new level
    for handler in logging.getLogger().handlers:
        if not isinstance(handler, logging.StreamHandler) or hasattr(handler, 'baseFilename'):
            handler.setLevel(level)
    logging.getLogger(__name__).info("Log level changed to %s", level)
    return {"level": level}
