"""Session management API routes."""

import logging
from datetime import datetime
import os
import re
from pathlib import Path
from typing import TYPE_CHECKING, Any

from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel, Field

from ieum.approval_policy import is_valid_approval_policy, normalize_approval_policy
from ieum.server.auth import get_request_user_id
from ieum.sessions import load_env_file, load_web_settings, save_env_file, save_web_settings

if TYPE_CHECKING:
    from ieum.server.services.session_service import SessionService

logger = logging.getLogger(__name__)

router = APIRouter()


def get_session_service(request: Request) -> Any:
    """Dependency to get the session service."""
    return request.app.state.session_service


class SessionCreate(BaseModel):
    """Request body for creating a session."""

    working_directory: str = Field(default=".", description="Working directory for the session")
    agent_id: str = Field(default="agent", description="Agent identifier")
    model_name: str | None = Field(default=None, description="Model to use (auto-detected if None)")
    auto_approve: bool = Field(default=False, description="Auto-approve tool calls")
    workspace_id: str = Field(default="default", description="Workspace identifier for sandbox isolation")
    approval_policy: str | None = Field(
        default=None,
        description="Approval preset policy (manual/read_only/safe_write/full_trusted)",
    )


class SessionInfo(BaseModel):
    """Session information response."""

    id: str
    agent_id: str
    working_directory: str
    model_name: str | None
    auto_approve: bool
    approval_policy: str
    timeout_minutes: int | None = None
    max_concurrent_sessions: int | None = None
    rate_limit_max_requests: int | None = None
    rate_limit_window_seconds: int | None = None
    user_id: str
    workspace_id: str
    created_at: datetime
    last_activity: datetime
    status: str


class SessionListResponse(BaseModel):
    """Response for listing sessions."""

    sessions: list[SessionInfo]
    total: int


def get_config(request: Request) -> Any:
    """Dependency to get the server config."""
    return request.app.state.config


_SAFE_SEGMENT_RE = re.compile(r"[^a-zA-Z0-9_.-]+")


def _safe_segment(value: str, *, fallback: str) -> str:
    """Sanitize a path segment used for sandboxed user/workspace roots."""
    raw = (value or "").strip()
    if not raw:
        return fallback
    sanitized = _SAFE_SEGMENT_RE.sub("_", raw).strip("._-")
    return (sanitized or fallback)[:64]


def _resolve_working_directory(
    request: Request,
    requested_dir: str,
    *,
    user_id: str,
    workspace_id: str,
    sandbox_root_override: str | None = None,
) -> str:
    """Resolve and validate session working directory."""
    config = request.app.state.config

    # Sandbox mode (recommended for central remote deployment)
    sandbox_root_value = sandbox_root_override or config.sandbox_root
    if sandbox_root_value:
        sandbox_root = Path(sandbox_root_value).resolve()
        safe_user = _safe_segment(user_id, fallback="anonymous")
        safe_workspace = _safe_segment(workspace_id, fallback="default")
        workspace_root = (sandbox_root / safe_user / safe_workspace).resolve()
        workspace_root.mkdir(parents=True, exist_ok=True)

        requested = (requested_dir or ".").strip()
        if requested in {"", "."}:
            return str(workspace_root)

        requested_path = Path(requested)
        if requested_path.is_absolute():
            raise HTTPException(
                status_code=400,
                detail="Absolute paths are not allowed when sandbox_root is enabled.",
            )

        resolved = (workspace_root / requested_path).resolve()
        try:
            resolved.relative_to(workspace_root)
        except ValueError as e:
            raise HTTPException(
                status_code=400,
                detail="Working directory escapes sandbox root.",
            ) from e
        resolved.mkdir(parents=True, exist_ok=True)
        return str(resolved)

    # Non-sandbox fallback -- still constrain to the configured working
    # directory (or CWD) to prevent arbitrary filesystem access.
    base_dir = config.working_directory or os.getcwd()
    if requested_dir == ".":
        return base_dir

    requested_path = Path(requested_dir).resolve()
    base_path = Path(base_dir).resolve()
    try:
        requested_path.relative_to(base_path)
    except ValueError:
        raise HTTPException(
            status_code=400,
            detail="Working directory must be within the server's configured working directory.",
        )

    return str(requested_path)


@router.post("", response_model=SessionInfo, status_code=201)
async def create_session(
    request: Request,
    body: SessionCreate,
    session_service: Any = Depends(get_session_service),
) -> SessionInfo:
    """Create a new agent session.

    Args:
        request: FastAPI request.
        body: Session creation parameters.
        session_service: Injected session service.

    Returns:
        Created session information.
    """
    config = request.app.state.config
    user_id = get_request_user_id(request, required=config.require_user_identity)
    workspace_id = _safe_segment(body.workspace_id, fallback="default")
    policy_resolver = getattr(request.app.state, "session_policy_resolver", None)
    resolved_policy = (
        policy_resolver.resolve(user_id=user_id, workspace_id=workspace_id)
        if policy_resolver is not None
        else None
    )
    if body.approval_policy is not None and not is_valid_approval_policy(body.approval_policy):
        raise HTTPException(
            status_code=400,
            detail="Invalid approval policy.",
        )
    approval_policy = normalize_approval_policy(
        body.approval_policy or getattr(resolved_policy, "approval_policy", "manual")
    )
    working_directory = _resolve_working_directory(
        request,
        body.working_directory,
        user_id=user_id,
        workspace_id=workspace_id,
        sandbox_root_override=getattr(resolved_policy, "sandbox_root", None),
    )

    # If model_name not provided, try to load from global settings
    model_name = body.model_name
    api_key: str | None = None
    base_url: str | None = None
    if not model_name:
        raw_settings = await _load_raw_global_settings()
        if raw_settings and raw_settings.get("provider") and raw_settings.get("model"):
            model_name = f"{raw_settings['provider']}/{raw_settings['model']}"
            # Store credentials for session-scoped injection (no os.environ mutation)
            api_key = raw_settings.get("api_key")
            base_url = raw_settings.get("base_url")

    try:
        session = await session_service.create_session(
            working_directory=working_directory,
            agent_id=body.agent_id,
            model_name=model_name,
            auto_approve=body.auto_approve,
            approval_policy=approval_policy,
            timeout_minutes=getattr(resolved_policy, "timeout_minutes", None),
            max_concurrent_sessions=getattr(resolved_policy, "max_concurrent_sessions", None),
            rate_limit_max_requests=getattr(resolved_policy, "rate_limit_max_requests", None),
            rate_limit_window_seconds=getattr(resolved_policy, "rate_limit_window_seconds", None),
            user_id=user_id,
            workspace_id=workspace_id,
        )

        # Attach session-scoped credentials (used by initialize_agent)
        if api_key:
            session._api_key = api_key
        if base_url:
            session._base_url = base_url
        return SessionInfo(
            id=session.id,
            agent_id=session.agent_id,
            working_directory=session.working_directory,
            model_name=session.model_name,
            auto_approve=session.auto_approve,
            approval_policy=session.approval_policy,
            timeout_minutes=session.timeout_minutes,
            max_concurrent_sessions=session.max_concurrent_sessions,
            rate_limit_max_requests=session.rate_limit_max_requests,
            rate_limit_window_seconds=session.rate_limit_window_seconds,
            user_id=session.user_id,
            workspace_id=session.workspace_id,
            created_at=session.created_at,
            last_activity=session.last_activity,
            status=session.status,
        )
    except Exception:
        logger.exception("Failed to create session")
        raise HTTPException(status_code=500, detail="Internal server error") from None


@router.get("", response_model=SessionListResponse)
async def list_sessions(
    request: Request,
    session_service: Any = Depends(get_session_service),
) -> SessionListResponse:
    """List all active sessions.

    Args:
        session_service: Injected session service.

    Returns:
        List of active sessions.
    """
    config = request.app.state.config
    user_id = get_request_user_id(request, required=config.require_user_identity)
    sessions = session_service.list_sessions(user_id=user_id)
    return SessionListResponse(
        sessions=[
            SessionInfo(
                id=s.id,
                agent_id=s.agent_id,
                working_directory=s.working_directory,
                model_name=s.model_name,
                auto_approve=s.auto_approve,
                approval_policy=s.approval_policy,
                timeout_minutes=s.timeout_minutes,
                max_concurrent_sessions=s.max_concurrent_sessions,
                rate_limit_max_requests=s.rate_limit_max_requests,
                rate_limit_window_seconds=s.rate_limit_window_seconds,
                user_id=s.user_id,
                workspace_id=s.workspace_id,
                created_at=s.created_at,
                last_activity=s.last_activity,
                status=s.status,
            )
            for s in sessions
        ],
        total=len(sessions),
    )


@router.get("/{session_id}", response_model=SessionInfo)
async def get_session(
    request: Request,
    session_id: str,
    session_service: Any = Depends(get_session_service),
) -> SessionInfo:
    """Get session details.

    Args:
        session_id: Session ID.
        session_service: Injected session service.

    Returns:
        Session information.
    """
    config = request.app.state.config
    user_id = get_request_user_id(request, required=config.require_user_identity)
    session = session_service.get_session(session_id, user_id=user_id)
    if session is None:
        raise HTTPException(status_code=404, detail=f"Session {session_id} not found")

    return SessionInfo(
        id=session.id,
        agent_id=session.agent_id,
        working_directory=session.working_directory,
        model_name=session.model_name,
        auto_approve=session.auto_approve,
        approval_policy=session.approval_policy,
        timeout_minutes=session.timeout_minutes,
        max_concurrent_sessions=session.max_concurrent_sessions,
        rate_limit_max_requests=session.rate_limit_max_requests,
        rate_limit_window_seconds=session.rate_limit_window_seconds,
        user_id=session.user_id,
        workspace_id=session.workspace_id,
        created_at=session.created_at,
        last_activity=session.last_activity,
        status=session.status,
    )


@router.delete("/{session_id}", status_code=204)
async def delete_session(
    request: Request,
    session_id: str,
    session_service: Any = Depends(get_session_service),
) -> None:
    """Delete a session.

    Args:
        session_id: Session ID to delete.
        session_service: Injected session service.
    """
    config = request.app.state.config
    user_id = get_request_user_id(request, required=config.require_user_identity)
    success = await session_service.delete_session(session_id, user_id=user_id)
    if not success:
        raise HTTPException(status_code=404, detail=f"Session {session_id} not found")


class ApprovalPolicyUpdate(BaseModel):
    """Request body for updating session approval policy."""

    approval_policy: str = Field(description="manual|read_only|safe_write|full_trusted")


class ApprovalPolicyResponse(BaseModel):
    """Response body for session approval policy updates."""

    success: bool
    approval_policy: str


@router.patch("/{session_id}/approval-policy", response_model=ApprovalPolicyResponse)
async def update_session_approval_policy(
    request: Request,
    session_id: str,
    body: ApprovalPolicyUpdate,
    session_service: Any = Depends(get_session_service),
) -> ApprovalPolicyResponse:
    """Update approval policy for an active session."""
    config = request.app.state.config
    user_id = get_request_user_id(request, required=config.require_user_identity)

    if not is_valid_approval_policy(body.approval_policy):
        raise HTTPException(
            status_code=400,
            detail="Invalid approval policy.",
        )
    normalized_policy = normalize_approval_policy(body.approval_policy)

    session = await session_service.update_session_approval_policy(
        session_id,
        normalized_policy,
        user_id=user_id,
    )
    if session is None:
        raise HTTPException(status_code=404, detail=f"Session {session_id} not found")
    return ApprovalPolicyResponse(success=True, approval_policy=session.approval_policy)


class AuditEvent(BaseModel):
    """Audit event payload."""

    id: int
    session_id: str
    user_id: str
    workspace_id: str
    event_type: str
    event_ts: str
    payload: dict[str, Any]


class AuditEventsResponse(BaseModel):
    """Audit event list response."""

    events: list[AuditEvent]
    total: int


class AuditReplayRequest(BaseModel):
    """Replay request body."""

    since_id: int | None = None
    limit: int = 200
    broadcast: bool = False


class AuditReplayResponse(BaseModel):
    """Replay result payload."""

    replayed: int
    events: list[AuditEvent]


@router.get("/{session_id}/audit", response_model=AuditEventsResponse)
async def get_session_audit_events(
    request: Request,
    session_id: str,
    since_id: int | None = None,
    limit: int = 200,
    session_service: Any = Depends(get_session_service),
) -> AuditEventsResponse:
    """List audit events for the given session."""
    config = request.app.state.config
    user_id = get_request_user_id(request, required=config.require_user_identity)
    limit = max(1, min(limit, 1000))
    events = await session_service.list_audit_events(
        session_id=session_id,
        user_id=user_id,
        since_id=since_id,
        limit=limit,
    )
    return AuditEventsResponse(
        events=[AuditEvent(**event) for event in events],
        total=len(events),
    )


@router.post("/{session_id}/audit/replay", response_model=AuditReplayResponse)
async def replay_session_audit_events(
    request: Request,
    session_id: str,
    body: AuditReplayRequest,
    session_service: Any = Depends(get_session_service),
) -> AuditReplayResponse:
    """Replay audit events and optionally broadcast replayable events to live clients."""
    from ieum.server.websocket.protocol import MessageType, WSMessage

    config = request.app.state.config
    user_id = get_request_user_id(request, required=config.require_user_identity)
    events = await session_service.list_audit_events(
        session_id=session_id,
        user_id=user_id,
        since_id=body.since_id,
        limit=body.limit,
    )
    if body.broadcast:
        ws_manager = request.app.state.ws_manager
        replayable_types = {
            "tool.start": MessageType.TOOL_START,
            "tool.success": MessageType.TOOL_SUCCESS,
            "tool.error": MessageType.TOOL_ERROR,
            "file.changed": MessageType.FILE_CHANGED,
            "system.message": MessageType.SYSTEM_MESSAGE,
            "error": MessageType.ERROR,
        }
        for event in events:
            msg_type = replayable_types.get(event["event_type"])
            if msg_type is None:
                continue
            await ws_manager.broadcast(
                session_id,
                WSMessage(type=msg_type, data=dict(event["payload"])),
            )
    return AuditReplayResponse(
        replayed=len(events),
        events=[AuditEvent(**event) for event in events],
    )


class ModelSettings(BaseModel):
    """Model configuration settings."""

    provider: str = Field(description="Provider: ollama, vllm, lmstudio, openrouter, openai, anthropic")
    base_url: str | None = Field(default=None, description="Base URL for local providers")
    api_key: str | None = Field(default=None, description="API key for cloud providers")
    model: str = Field(description="Model name/ID")


class SettingsUpdate(BaseModel):
    """Request body for updating settings."""

    model_settings: ModelSettings


class SettingsResponse(BaseModel):
    """Settings response."""

    success: bool
    model_name: str


@router.patch("/{session_id}/settings", response_model=SettingsResponse)
async def update_session_settings(
    request: Request,
    session_id: str,
    body: SettingsUpdate,
    session_service: Any = Depends(get_session_service),
) -> SettingsResponse:
    """Update session settings including model configuration.

    Args:
        session_id: Session ID.
        body: Settings to update.
        session_service: Injected session service.

    Returns:
        Updated settings confirmation.
    """
    config = request.app.state.config
    user_id = get_request_user_id(request, required=config.require_user_identity)
    session = session_service.get_session(session_id, user_id=user_id)
    if session is None:
        raise HTTPException(status_code=404, detail=f"Session {session_id} not found")

    # Build model name string based on provider
    ms = body.model_settings
    if ms.provider in ("ollama", "vllm", "lmstudio"):
        # Local providers: provider/model or provider/model@base_url
        if ms.base_url:
            model_name = f"{ms.provider}/{ms.model}@{ms.base_url}"
        else:
            model_name = f"{ms.provider}/{ms.model}"
    else:
        # Cloud providers: provider/model (API key stored separately)
        model_name = f"{ms.provider}/{ms.model}"

    # Update session - reset agent so it gets recreated with new model
    session.model_name = model_name
    session.agent = None  # Force agent recreation
    session.backend = None

    # Store session-scoped credentials for agent initialization
    if ms.api_key:
        session._api_key = ms.api_key
    if ms.base_url:
        session._base_url = ms.base_url

    # Save settings to SQLite for persistence
    await save_web_settings({
        "provider": ms.provider,
        "base_url": ms.base_url,
        "api_key": ms.api_key,
        "model": ms.model,
    })

    return SettingsResponse(success=True, model_name=model_name)


class GlobalSettingsResponse(BaseModel):
    """Global settings response."""

    provider: str | None = None
    base_url: str | None = None
    has_api_key: bool = False
    api_key_hint: str | None = None
    model: str | None = None
    has_github_pat: bool = False
    github_pat_hint: str | None = None
    github_host: str | None = None


async def _load_raw_global_settings() -> dict | None:
    """Load raw global settings dict (with secrets) for internal use only."""
    settings = await load_web_settings()
    if settings:
        return settings
    settings = load_env_file("local")
    if settings:
        return settings
    settings = load_env_file("global")
    if settings:
        return settings
    return None


def _mask_secret(value: str | None) -> tuple[bool, str | None]:
    """Return (has_value, masked_hint) for a secret string."""
    if not value:
        return False, None
    if len(value) <= 8:
        return True, value[:2] + "..." + value[-2:]
    return True, value[:4] + "..." + value[-4:]


@router.get("/settings/global", response_model=GlobalSettingsResponse)
async def get_global_settings() -> GlobalSettingsResponse:
    """Get saved global settings.

    Tries to load from: 1) SQLite, 2) local .env, 3) global ~/.ieum/.env

    Returns:
        Saved model settings or empty if none saved.
    """
    def _build_response(settings: dict) -> GlobalSettingsResponse:
        has_key, key_hint = _mask_secret(settings.get("api_key"))
        has_pat, pat_hint = _mask_secret(settings.get("github_pat"))
        return GlobalSettingsResponse(
            provider=settings.get("provider"),
            base_url=settings.get("base_url"),
            has_api_key=has_key,
            api_key_hint=key_hint,
            model=settings.get("model"),
            has_github_pat=has_pat,
            github_pat_hint=pat_hint,
            github_host=settings.get("github_host"),
        )

    # Try SQLite first
    settings = await load_web_settings()
    if settings:
        return _build_response(settings)

    # Try local .env (project folder)
    settings = load_env_file("local")
    if settings:
        return _build_response(settings)

    # Try global .env (~/.ieum/.env)
    settings = load_env_file("global")
    if settings:
        return _build_response(settings)

    return GlobalSettingsResponse()


class GlobalSettingsUpdate(BaseModel):
    """Request body for updating global settings."""

    provider: str = Field(description="Provider: ollama, vllm, lmstudio, openrouter, openai, anthropic")
    base_url: str | None = Field(default=None, description="Base URL for local providers")
    api_key: str | None = Field(default=None, description="API key for cloud providers")
    model: str = Field(description="Model name/ID")
    github_pat: str | None = Field(default=None, description="GitHub Personal Access Token")
    github_host: str | None = Field(default=None, description="GitHub Host (for Enterprise)")
    save_to: str = Field(default="global", description="Where to save: 'global' (~/.ieum/.env) or 'local' (project/.env)")


class GlobalSettingsResponseWithPath(BaseModel):
    """Global settings response with env file path."""

    provider: str | None = None
    base_url: str | None = None
    has_api_key: bool = False
    api_key_hint: str | None = None
    model: str | None = None
    has_github_pat: bool = False
    github_pat_hint: str | None = None
    github_host: str | None = None
    env_path: str | None = None


@router.post("/settings/global", response_model=GlobalSettingsResponseWithPath)
async def save_global_settings(body: GlobalSettingsUpdate) -> GlobalSettingsResponseWithPath:
    """Save global settings to SQLite and .env file.

    Args:
        body: Settings to save.

    Returns:
        Saved settings confirmation with env file path.
    """
    settings_dict = {
        "provider": body.provider,
        "base_url": body.base_url,
        "api_key": body.api_key,
        "model": body.model,
        "github_pat": body.github_pat,
        "github_host": body.github_host,
    }

    # Save to SQLite (for session persistence)
    await save_web_settings(settings_dict)

    # Save to .env file (for persistence across restarts)
    env_path = save_env_file(settings_dict, target=body.save_to)

    # NOTE: We intentionally do NOT mutate os.environ here.
    # API keys, base URLs, and model names are persisted to SQLite / .env
    # and injected into sessions on a per-session basis via Session._api_key
    # and Session._base_url.  This avoids race conditions when multiple
    # sessions run concurrently with different credentials.
    #
    # Existing sessions will pick up new credentials on next agent
    # re-initialization (session.agent = None forces re-creation).

    has_key, key_hint = _mask_secret(body.api_key)
    has_pat, pat_hint = _mask_secret(body.github_pat)
    return GlobalSettingsResponseWithPath(
        provider=body.provider,
        base_url=body.base_url,
        has_api_key=has_key,
        api_key_hint=key_hint,
        model=body.model,
        has_github_pat=has_pat,
        github_pat_hint=pat_hint,
        github_host=body.github_host,
        env_path=env_path,
    )
