#!/usr/bin/env python3
"""
OAuth Token Refresher Service

This service monitors OAuth tokens in the .oauth-tokens directory and automatically
refreshes them before they expire. It runs continuously in the background, checking
tokens every configurable interval (default 5 minutes).

Usage:
    uv run python credentials-provider/token_refresher.py                    # Run with defaults
    uv run python credentials-provider/token_refresher.py --interval 300     # Check every 5 minutes
    uv run python credentials-provider/token_refresher.py --buffer 3600      # Refresh 1 hour before expiry
    uv run python credentials-provider/token_refresher.py --once             # Run once and exit
    uv run python credentials-provider/token_refresher.py --once --force     # Force refresh all tokens once and exit
    nohup uv run python credentials-provider/token_refresher.py > token_refresher.log 2>&1 &  # Run in background
"""

import argparse
import json
import logging
import os
import signal
import subprocess  # nosec B404
import sys
import tempfile
import time
from pathlib import Path

import psutil

# Configure logging with basicConfig
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s,p%(process)s,{%(filename)s:%(lineno)d},%(levelname)s,%(message)s",
)
logger = logging.getLogger(__name__)


def _load_env_file() -> None:
    """Load environment variables from .env file in project root."""
    # Get the project root directory (parent of credentials-provider)
    script_dir = Path(__file__).parent
    project_root = script_dir.parent
    env_file = project_root / ".env"

    if env_file.exists():
        try:
            with open(env_file) as f:
                for line in f:
                    line = line.strip()
                    if line and not line.startswith("#") and "=" in line:
                        key, value = line.split("=", 1)
                        # Remove quotes if present
                        value = value.strip('"').strip("'")
                        os.environ[key] = value
            logger.debug(f"Loaded environment variables from {env_file}")
        except Exception as e:
            logger.warning(f"Failed to load .env file: {e}")
    else:
        logger.debug(f"No .env file found at {env_file}")


# Configuration constants
DEFAULT_CHECK_INTERVAL = 300  # 5 minutes in seconds
DEFAULT_EXPIRY_BUFFER = 3600  # 1 hour buffer before expiry

# Process management
PIDFILE_NAME = "token_refresher.pid"

# Dynamically determine paths relative to this script's location
SCRIPT_DIR = Path(__file__).parent
PROJECT_ROOT = SCRIPT_DIR.parent
OAUTH_TOKENS_DIR = PROJECT_ROOT / ".oauth-tokens"
CREDENTIALS_PROVIDER_DIR = SCRIPT_DIR

# Files to ignore during token refresh (derived files that get regenerated)
IGNORED_FILES = {
    "mcp.json",
    "vscode_mcp.json",
    "*readable*",  # Any file with "readable" in the name
}


def _should_ignore_file(filename: str) -> bool:
    """
    Check if a token file should be ignored.

    Args:
        filename: Name of the token file

    Returns:
        True if file should be ignored, False otherwise
    """
    # Check exact matches
    if filename in {"mcp.json", "vscode_mcp.json"}:
        return True

    # Check for "readable" in filename
    if "readable" in filename.lower():
        return True

    return False


def _parse_token_file(filepath: Path) -> dict | None:
    """
    Parse a token JSON file and extract relevant information.

    Args:
        filepath: Path to the token file

    Returns:
        Token data dict or None if file cannot be parsed
    """
    try:
        with open(filepath) as f:
            data = json.load(f)

        # Validate required fields
        if "expires_at" not in data:
            logger.debug(f"No expires_at field in {filepath.name}")
            return None

        return data
    except (OSError, json.JSONDecodeError) as e:
        logger.warning(f"Failed to parse {filepath.name}: {e}")
        return None


def _get_all_tokens() -> list[tuple[Path, dict]]:
    """
    Get all valid token files regardless of expiration status.

    Returns:
        List of (filepath, token_data) tuples for all valid tokens
    """
    if not OAUTH_TOKENS_DIR.exists():
        logger.error("OAuth tokens directory not found")
        return []

    all_tokens = []

    for filepath in OAUTH_TOKENS_DIR.glob("*.json"):
        # Skip ignored files
        if _should_ignore_file(filepath.name):
            logger.debug(f"Ignoring file: {filepath.name}")
            continue

        # Parse token file
        token_data = _parse_token_file(filepath)
        if not token_data:
            continue

        logger.info(f"Found token file: {filepath.name}")
        logger.debug(f"Reading token from: {filepath.absolute()}")
        all_tokens.append((filepath, token_data))

    return all_tokens


def _get_expiring_tokens(buffer_seconds: int = DEFAULT_EXPIRY_BUFFER) -> list[tuple[Path, dict]]:
    """
    Find all tokens that are expired or will expire within the buffer period.

    Args:
        buffer_seconds: Number of seconds before expiry to trigger refresh

    Returns:
        List of (filepath, token_data) tuples for expiring tokens
    """
    if not OAUTH_TOKENS_DIR.exists():
        logger.error(f"OAuth tokens directory not found: {OAUTH_TOKENS_DIR}")
        return []

    current_time = time.time()
    expiring_tokens = []

    for filepath in OAUTH_TOKENS_DIR.glob("*.json"):
        # Skip ignored files
        if _should_ignore_file(filepath.name):
            logger.debug(f"Ignoring file: {filepath.name}")
            continue

        # Parse token file
        token_data = _parse_token_file(filepath)
        if not token_data:
            continue

        logger.debug(f"Reading token from: {filepath.absolute()}")

        # Check expiration
        expires_at = token_data.get("expires_at", 0)

        # Convert ISO timestamp to Unix timestamp if needed
        if isinstance(expires_at, str):
            try:
                from datetime import datetime

                # Parse ISO timestamp and convert to Unix timestamp
                expires_at_dt = datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
                expires_at = expires_at_dt.timestamp()
            except (ValueError, AttributeError) as e:
                logger.warning(
                    f"Could not parse expires_at timestamp '{expires_at}' in {filepath.name}: {e}"
                )
                continue

        time_until_expiry = expires_at - current_time

        if time_until_expiry <= buffer_seconds:
            hours_until_expiry = time_until_expiry / 3600
            if time_until_expiry <= 0:
                logger.warning(
                    f"Token EXPIRED: {filepath.name} (expired {-hours_until_expiry:.1f} hours ago)"
                )
            else:
                logger.info(
                    f"Token expiring soon: {filepath.name} (expires in {hours_until_expiry:.1f} hours)"
                )
            logger.debug(f"Will refresh token at: {filepath.absolute()}")
            expiring_tokens.append((filepath, token_data))

    return expiring_tokens


def _determine_refresh_method(token_data: dict, filename: str) -> str | None:
    """
    Determine which refresh method to use based on token data.

    Args:
        token_data: Parsed token data
        filename: Token filename

    Returns:
        Refresh method ('agentcore' or 'oauth') or None if cannot determine
    """
    provider = token_data.get("provider", "").lower()

    # Check for AgentCore/Bedrock tokens
    if "bedrock" in provider or "agentcore" in provider:
        return "agentcore"

    # Check for OAuth providers (including Keycloak and Cognito M2M)
    oauth_providers = ["atlassian", "google", "github", "microsoft", "oauth", "keycloak", "cognito"]
    if any(p in provider for p in oauth_providers):
        return "oauth"

    # Try to infer from filename
    if "bedrock" in filename.lower() or "agentcore" in filename.lower():
        return "agentcore"

    if "egress" in filename.lower() or "ingress" in filename.lower():
        return "oauth"

    logger.warning(f"Cannot determine refresh method for {filename} with provider '{provider}'")
    return None


def _refresh_agentcore_token(token_data: dict, filename: str) -> bool:
    """
    Refresh a Bedrock AgentCore token using generate_access_token.py.

    Args:
        token_data: Current token data
        filename: Token filename

    Returns:
        True if refresh successful, False otherwise
    """
    script_path = CREDENTIALS_PROVIDER_DIR / "agentcore-auth" / "generate_access_token.py"

    if not script_path.exists():
        logger.error(f"AgentCore refresh script not found: {script_path}")
        return False

    try:
        # Extract server name from filename if possible
        # Format: bedrock-agentcore-{server_name}-egress.json
        server_name = None
        if filename.startswith("bedrock-agentcore-") and filename.endswith("-egress.json"):
            server_name = filename.replace("bedrock-agentcore-", "").replace("-egress.json", "")

        logger.info(f"Refreshing AgentCore token for: {server_name or 'default'}")

        # Run the refresh script using uv run
        cmd = ["uv", "run", "python", str(script_path)]
        if server_name:
            # The script might accept server-specific parameters
            # Check the script for available options
            pass

        logger.debug(f"Running AgentCore refresh command: {' '.join(cmd)}")
        logger.debug(f"Working directory: {PROJECT_ROOT.absolute()}")

        result = subprocess.run(  # nosec B603 - internal script path via uv run, no user input
            cmd, cwd=PROJECT_ROOT, capture_output=True, text=True, timeout=30
        )

        if result.returncode == 0:
            logger.info(f"Successfully refreshed AgentCore token: {filename}")
            return True
        else:
            logger.error(f"Failed to refresh AgentCore token: {result.stderr}")
            return False

    except subprocess.TimeoutExpired:
        logger.error(f"Timeout refreshing AgentCore token: {filename}")
        return False
    except Exception as e:
        logger.error(f"Error refreshing AgentCore token {filename}: {e}")
        return False


def _refresh_oauth_token(token_data: dict, filename: str) -> bool:
    """
    Refresh a generic OAuth token using egress_oauth.py or ingress_oauth.py.

    Args:
        token_data: Current token data
        filename: Token filename

    Returns:
        True if refresh successful, False otherwise
    """
    # Determine which OAuth script to use
    provider = token_data.get("provider", "atlassian")

    if "ingress" in filename.lower() or provider == "keycloak":
        script_name = "ingress_oauth.py"
        # Ingress supports both Cognito and Keycloak M2M, doesn't use --provider argument
        use_provider_arg = False
    else:
        script_name = "egress_oauth.py"  # Default to egress
        use_provider_arg = True

    script_path = CREDENTIALS_PROVIDER_DIR / "oauth" / script_name

    if not script_path.exists():
        logger.error(f"OAuth refresh script not found: {script_path}")
        return False

    try:
        logger.info(f"Refreshing OAuth token for provider: {provider}")

        # Build command based on script type
        cmd = ["uv", "run", "python", str(script_path)]

        # Only add --provider for egress OAuth (not ingress)
        # Ingress OAuth auto-detects Cognito vs Keycloak based on AUTH_PROVIDER env var
        if use_provider_arg:
            cmd.extend(["--provider", provider])

        logger.debug(f"Running OAuth refresh command: {' '.join(cmd)}")
        logger.debug(f"Working directory: {PROJECT_ROOT.absolute()}")

        # For Keycloak and Cognito M2M tokens, we don't typically have refresh tokens
        # The client_credentials flow will generate a new token
        if provider in ["keycloak_m2m", "cognito_m2m"]:
            logger.info(f"M2M token detected ({provider}), using client_credentials flow")
        elif "refresh_token" in token_data:
            logger.info("Refresh token available, script will handle refresh flow")

        result = subprocess.run(  # nosec B603 - internal script path via uv run, no user input
            cmd,
            cwd=PROJECT_ROOT,
            capture_output=True,
            text=True,
            timeout=60,  # OAuth flow might take longer
        )

        if result.returncode == 0:
            logger.info(f"Successfully refreshed OAuth token: {filename}")
            return True
        else:
            logger.error(f"Failed to refresh OAuth token: {result.stderr}")
            return False

    except subprocess.TimeoutExpired:
        logger.error(f"Timeout refreshing OAuth token: {filename}")
        return False
    except Exception as e:
        logger.error(f"Error refreshing OAuth token {filename}: {e}")
        return False


def _refresh_token(filepath: Path, token_data: dict) -> bool:
    """
    Refresh a single token based on its type.

    Args:
        filepath: Path to the token file
        token_data: Parsed token data

    Returns:
        True if refresh successful, False otherwise
    """
    filename = filepath.name
    refresh_method = _determine_refresh_method(token_data, filename)

    if not refresh_method:
        logger.error(f"Cannot determine how to refresh {filename}")
        return False

    if refresh_method == "agentcore":
        return _refresh_agentcore_token(token_data, filename)
    elif refresh_method == "oauth":
        return _refresh_oauth_token(token_data, filename)
    else:
        logger.error(f"Unknown refresh method: {refresh_method}")
        return False


def _scan_noauth_services() -> list[dict]:
    """
    Scan registry servers and find services with auth_scheme: none.

    Returns:
        List of no-auth service configurations
    """
    registry_dir = PROJECT_ROOT / "registry" / "servers"
    noauth_services = []

    if not registry_dir.exists():
        logger.warning(f"Registry servers directory not found: {registry_dir}")
        return []

    logger.debug(f"Scanning for no-auth services in: {registry_dir}")

    for json_file in registry_dir.glob("*.json"):
        # Skip server_state.json
        if json_file.name == "server_state.json":
            continue

        try:
            with open(json_file) as f:
                server_config = json.load(f)

            # Backward-compatible read: prefer auth_scheme, fall back to auth_type
            auth_scheme = server_config.get("auth_scheme", server_config.get("auth_type", "none"))
            if auth_scheme == "none":
                # Extract relevant service information
                service = {
                    "server_name": server_config.get("server_name", "Unknown"),
                    "path": server_config.get("path", ""),
                    "proxy_pass_url": server_config.get("proxy_pass_url", ""),
                    "supported_transports": server_config.get(
                        "supported_transports", ["streamable-http"]
                    ),
                    "description": server_config.get("description", ""),
                    "file_name": json_file.name,
                }
                noauth_services.append(service)
                logger.debug(f"Found no-auth service: {service['server_name']} ({service['path']})")
        except (OSError, json.JSONDecodeError) as e:
            logger.warning(f"Failed to parse {json_file.name}: {e}")
            continue

    return noauth_services


def _regenerate_mcp_configs() -> bool:
    """
    Regenerate MCP configuration files (mcp.json and vscode_mcp.json) after token refresh.

    Returns:
        True if regeneration successful, False otherwise
    """
    logger.info("Regenerating MCP configuration files...")

    try:
        # Check for required files
        ingress_file = OAUTH_TOKENS_DIR / "ingress.json"
        has_ingress = ingress_file.exists()

        # Find all egress token files
        egress_files = []
        for file_path in OAUTH_TOKENS_DIR.glob("*-egress.json"):
            if file_path.is_file():
                egress_files.append(file_path)
                logger.debug(f"Found egress token file: {file_path.name}")

        # Scan for no-auth services
        noauth_services = _scan_noauth_services()
        logger.info(f"Found {len(noauth_services)} no-auth services to include")

        if not has_ingress and not egress_files and not noauth_services:
            logger.warning(
                "No token files or no-auth services found, skipping MCP configuration generation"
            )
            return True

        # Generate both configurations
        vscode_success = _generate_vscode_config(
            has_ingress, ingress_file, egress_files, noauth_services
        )
        roocode_success = _generate_roocode_config(
            has_ingress, ingress_file, egress_files, noauth_services
        )

        if vscode_success and roocode_success:
            logger.info("MCP configuration files regenerated successfully")
            return True
        else:
            logger.error("Failed to regenerate some MCP configuration files")
            return False

    except Exception as e:
        logger.error(f"Error regenerating MCP configs: {e}")
        return False


def _get_ingress_headers(ingress_file: Path) -> dict[str, str]:
    """
    Extract ingress authentication headers from token file.

    Args:
        ingress_file: Path to ingress token file

    Returns:
        Dictionary of ingress headers
    """
    headers = {}

    # Check AUTH_PROVIDER from environment
    auth_provider = os.environ.get("AUTH_PROVIDER", "")

    if auth_provider == "keycloak":
        # When using Keycloak, get token from agent token file
        agent_token_file = OAUTH_TOKENS_DIR / "agent-ai-coding-assistant-m2m-token.json"
        if agent_token_file.exists():
            try:
                with open(agent_token_file) as f:
                    agent_data = json.load(f)

                if agent_data and agent_data.get("access_token"):
                    logger.debug("Using Keycloak agent token for ingress authentication")
                    headers = {"X-Authorization": f"Bearer {agent_data.get('access_token', '')}"}

                    # Add Keycloak-specific headers
                    headers.update(
                        {
                            "X-Client-Id": agent_data.get("client_id", ""),
                            "X-Keycloak-Realm": agent_data.get("keycloak_realm", ""),
                            "X-Keycloak-URL": agent_data.get("keycloak_url", ""),
                        }
                    )
                    logger.debug(
                        f"Using Keycloak agent headers: client_id={agent_data.get('client_id', '')}"
                    )
                    return headers
            except (OSError, json.JSONDecodeError) as e:
                logger.warning(f"Failed to read Keycloak agent token file: {e}")

        # Fall back to ingress file if agent token not available
        logger.warning("Keycloak agent token not available, falling back to ingress token")

    # Default behavior: use ingress file
    if ingress_file.exists():
        try:
            with open(ingress_file) as f:
                ingress_data = json.load(f)

                # Always include the access token
                headers["X-Authorization"] = f"Bearer {ingress_data.get('access_token', '')}"

                # Add provider-specific headers
                provider = ingress_data.get("provider", "cognito_m2m")
                logger.debug(f"Detected ingress provider: {provider}")

                if provider == "keycloak_m2m":
                    # Keycloak-specific headers
                    headers.update(
                        {
                            "X-Client-Id": ingress_data.get("client_id", ""),
                            "X-Keycloak-Realm": ingress_data.get("realm", ""),
                            "X-Keycloak-URL": ingress_data.get("keycloak_url", ""),
                        }
                    )
                    logger.debug(f"Using Keycloak headers: realm={ingress_data.get('realm', '')}")
                else:  # cognito_m2m (default)
                    # Cognito-specific headers
                    headers.update(
                        {
                            "X-User-Pool-Id": ingress_data.get("user_pool_id", ""),
                            "X-Client-Id": ingress_data.get("client_id", ""),
                            "X-Region": ingress_data.get("region", "us-east-1"),
                        }
                    )
                    logger.debug(
                        f"Using Cognito headers: pool_id={ingress_data.get('user_pool_id', '')}"
                    )

        except (OSError, json.JSONDecodeError) as e:
            logger.warning(f"Failed to read ingress file: {e}")

    return headers


def _create_egress_server_config(
    egress_file: Path,
    ingress_headers: dict[str, str],
    registry_url: str,
    config_type: str = "vscode",
) -> tuple[str, dict]:
    """
    Create server configuration from egress token file.

    Args:
        egress_file: Path to egress token file
        ingress_headers: Ingress authentication headers
        registry_url: Base registry URL
        config_type: Either "vscode" or "roocode"

    Returns:
        Tuple of (server_key, server_config)
    """
    try:
        with open(egress_file) as f:
            egress_data = json.load(f)
    except (OSError, json.JSONDecodeError) as e:
        logger.warning(f"Failed to read egress file {egress_file.name}: {e}")
        return None, None

    provider = egress_data.get("provider", "")
    token = egress_data.get("access_token", "")
    cloud_id = egress_data.get("cloud_id", "")

    # Determine server key and URL
    if provider == "atlassian":
        server_key = "atlassian"
        headers = {"Authorization": f"Bearer {token}"}
        if cloud_id:
            headers["X-Atlassian-Cloud-Id"] = cloud_id
        if ingress_headers:
            headers.update(ingress_headers)
        url = f"{registry_url}/atlassian/mcp"

    elif provider == "bedrock-agentcore":
        # Extract server name from filename
        filename = egress_file.name
        if filename.startswith("bedrock-agentcore-") and filename.endswith("-egress.json"):
            server_key = filename.replace("bedrock-agentcore-", "").replace("-egress.json", "")
        else:
            server_key = "sre-gateway"

        headers = {"Authorization": f"Bearer {token}"}
        if ingress_headers:
            headers.update(ingress_headers)
        url = f"{registry_url}/{server_key}/mcp"

    else:
        # Generic provider
        server_key = provider
        headers = {"Authorization": f"Bearer {token}"}
        if ingress_headers:
            headers.update(ingress_headers)
        url = f"{registry_url}/{provider}/mcp"

    # Create config based on type
    if config_type == "vscode":
        server_config = {"url": url, "headers": headers}
    else:  # roocode
        server_config = {
            "type": "streamable-http",
            "url": url,
            "headers": headers,
            "disabled": False,
            "alwaysAllow": [],
        }

    return server_key, server_config


def _create_noauth_server_config(
    service: dict, ingress_headers: dict[str, str], registry_url: str, config_type: str = "vscode"
) -> tuple[str, dict]:
    """
    Create server configuration for no-auth service.

    Args:
        service: No-auth service information
        ingress_headers: Ingress authentication headers
        registry_url: Base registry URL
        config_type: Either "vscode" or "roocode"

    Returns:
        Tuple of (server_key, server_config)
    """
    # Use path as server key (remove leading and trailing slashes)
    server_key = service["path"].strip("/")
    if not server_key:
        return None, None

    # Construct service URL
    path = service["path"].rstrip("/")
    service_url = f"{registry_url}{path}/mcp"

    # Create config based on type
    if config_type == "vscode":
        server_config = {"url": service_url}
        if ingress_headers:
            server_config["headers"] = ingress_headers
    else:  # roocode
        # Determine transport type
        supported_transports = service.get("supported_transports", ["streamable-http"])
        transport_type = supported_transports[0] if supported_transports else "streamable-http"

        server_config = {
            "type": transport_type,
            "url": service_url,
            "disabled": False,
            "alwaysAllow": [],
        }
        if ingress_headers:
            server_config["headers"] = ingress_headers

    return server_key, server_config


def _generate_vscode_config(
    has_ingress: bool,
    ingress_file: Path,
    egress_files: list[Path],
    noauth_services: list[dict] = None,
) -> bool:
    """
    Generate VS Code MCP configuration file.

    Args:
        has_ingress: Whether ingress token is available
        ingress_file: Path to ingress token file
        egress_files: List of egress token file paths
        noauth_services: List of no-auth service configurations

    Returns:
        True if generation successful, False otherwise
    """
    config_file = OAUTH_TOKENS_DIR / "vscode_mcp.json"

    try:
        with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".json") as temp_file:
            temp_path = temp_file.name

            # Default registry URL
            registry_url = os.getenv("REGISTRY_URL", "https://mcpgateway.ddns.net")

            # Initialize configuration
            config = {"mcp": {"servers": {}}}

            # Get ingress headers
            ingress_headers = _get_ingress_headers(ingress_file) if has_ingress else {}

            # Process egress files
            for egress_file in egress_files:
                server_key, server_config = _create_egress_server_config(
                    egress_file, ingress_headers, registry_url, "vscode"
                )
                if server_key and server_config:
                    config["mcp"]["servers"][server_key] = server_config
                    logger.debug(f"Added egress service {server_key} to VS Code config")

            # Process no-auth services
            if noauth_services:
                for service in noauth_services:
                    server_key, server_config = _create_noauth_server_config(
                        service, ingress_headers, registry_url, "vscode"
                    )

                    # Skip if already added or invalid
                    if not server_key or server_key in config["mcp"]["servers"]:
                        continue

                    config["mcp"]["servers"][server_key] = server_config
                    logger.debug(f"Added no-auth service {server_key} to VS Code config")

            # Write JSON to temp file
            json.dump(config, temp_file, indent=2)

        # Move temp file to final location and set permissions
        os.rename(temp_path, config_file)
        os.chmod(config_file, 0o600)

        logger.info(f"Generated VS Code MCP config: {config_file}")
        logger.debug(f"VS Code config written to: {config_file.absolute()}")
        return True

    except Exception as e:
        logger.error(f"Error generating VS Code MCP config: {e}")
        if "temp_path" in locals():
            try:
                os.unlink(temp_path)
            except Exception as e:
                logger.debug(f"Failed to clean up temp file: {e}")
        return False


def _generate_roocode_config(
    has_ingress: bool,
    ingress_file: Path,
    egress_files: list[Path],
    noauth_services: list[dict] = None,
) -> bool:
    """
    Generate Roocode MCP configuration file.

    Args:
        has_ingress: Whether ingress token is available
        ingress_file: Path to ingress token file
        egress_files: List of egress token file paths
        noauth_services: List of no-auth service configurations

    Returns:
        True if generation successful, False otherwise
    """
    config_file = OAUTH_TOKENS_DIR / "mcp.json"

    try:
        with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".json") as temp_file:
            temp_path = temp_file.name

            # Default registry URL
            registry_url = os.getenv("REGISTRY_URL", "https://mcpgateway.ddns.net")

            # Initialize configuration
            config = {"mcpServers": {}}

            # Get ingress headers
            ingress_headers = _get_ingress_headers(ingress_file) if has_ingress else {}

            # Process egress files
            for egress_file in egress_files:
                server_key, server_config = _create_egress_server_config(
                    egress_file, ingress_headers, registry_url, "roocode"
                )
                if server_key and server_config:
                    config["mcpServers"][server_key] = server_config
                    logger.debug(f"Added egress service {server_key} to Roocode config")

            # Process no-auth services
            if noauth_services:
                for service in noauth_services:
                    server_key, server_config = _create_noauth_server_config(
                        service, ingress_headers, registry_url, "roocode"
                    )

                    # Skip if already added or invalid
                    if not server_key or server_key in config["mcpServers"]:
                        continue

                    config["mcpServers"][server_key] = server_config
                    logger.debug(f"Added no-auth service {server_key} to Roocode config")

            # Write JSON to temp file
            json.dump(config, temp_file, indent=2)

        # Move temp file to final location and set permissions
        os.rename(temp_path, config_file)
        os.chmod(config_file, 0o600)

        logger.info(f"Generated Roocode MCP config: {config_file}")
        logger.debug(f"Roocode config written to: {config_file.absolute()}")
        return True

    except Exception as e:
        logger.error(f"Error generating Roocode MCP config: {e}")
        if "temp_path" in locals():
            try:
                os.unlink(temp_path)
            except Exception as e:
                logger.debug(f"Failed to clean up temp file: {e}")
        return False


def _run_refresh_cycle(
    buffer_seconds: int = DEFAULT_EXPIRY_BUFFER, force_refresh: bool = False
) -> None:
    """
    Run a single refresh cycle, checking and refreshing expiring tokens.

    Args:
        buffer_seconds: Number of seconds before expiry to trigger refresh
        force_refresh: If True, refresh all tokens regardless of expiration
    """
    logger.info("Starting token refresh cycle...")
    logger.debug(f"Token directory: {OAUTH_TOKENS_DIR.absolute()}")

    # Find expiring tokens
    if force_refresh:
        expiring_tokens = _get_all_tokens()
        logger.info("Force refresh enabled - will refresh all tokens")
    else:
        expiring_tokens = _get_expiring_tokens(buffer_seconds)

    if not expiring_tokens:
        logger.info("No tokens need refreshing")
        return

    logger.info(f"Found {len(expiring_tokens)} token(s) needing refresh")

    # Refresh each expiring token
    success_count = 0
    for filepath, token_data in expiring_tokens:
        logger.info(f"Attempting to refresh: {filepath.name}")
        logger.debug(f"Processing token file: {filepath.absolute()}")

        if _refresh_token(filepath, token_data):
            success_count += 1
            logger.info(f"Token successfully updated at: {filepath.absolute()}")
        else:
            logger.error(f"Failed to refresh: {filepath.name}")
            logger.error(f"Failed token location: {filepath.absolute()}")

    logger.info(
        f"Refresh cycle complete: {success_count}/{len(expiring_tokens)} tokens refreshed successfully"
    )

    # Regenerate MCP configuration files if any tokens were refreshed
    if success_count > 0:
        logger.info("Regenerating MCP configuration files after token refresh...")
        if _regenerate_mcp_configs():
            logger.info("MCP configuration files updated successfully")
        else:
            logger.error("Failed to update MCP configuration files")


def _get_pidfile_path() -> Path:
    """
    Get the path to the PID file for the token refresher service.

    Returns:
        Path to the PID file
    """
    return PROJECT_ROOT / "token_refresher.pid"


def _write_pidfile() -> None:
    """
    Write the current process PID to the PID file.
    """
    pidfile = _get_pidfile_path()
    with open(pidfile, "w") as f:
        f.write(str(os.getpid()))
    logger.debug(f"PID file written: {pidfile}")


def _remove_pidfile() -> None:
    """
    Remove the PID file if it exists.
    """
    pidfile = _get_pidfile_path()
    try:
        if pidfile.exists():
            pidfile.unlink()
            logger.debug(f"PID file removed: {pidfile}")
    except Exception as e:
        logger.warning(f"Failed to remove PID file: {e}")


def _kill_existing_instance() -> bool:
    """
    Kill any existing token refresher instance if running.

    Returns:
        True if an existing instance was killed, False if none was found
    """
    pidfile = _get_pidfile_path()

    if not pidfile.exists():
        logger.debug("No PID file found, no existing instance to kill")
        return False

    try:
        with open(pidfile) as f:
            old_pid = int(f.read().strip())

        # Check if process exists and is a token refresher
        if psutil.pid_exists(old_pid):
            try:
                process = psutil.Process(old_pid)
                cmdline = " ".join(process.cmdline())

                # Check if it's actually our token refresher process
                if "token_refresher.py" in cmdline:
                    logger.info(f"Found existing token refresher instance (PID: {old_pid})")
                    logger.info(f"Killing existing instance: {cmdline}")

                    # Try graceful shutdown first
                    process.terminate()
                    try:
                        process.wait(timeout=5)
                        logger.info(f"Gracefully terminated existing instance (PID: {old_pid})")
                    except psutil.TimeoutExpired:
                        # Force kill if graceful shutdown fails
                        logger.warning(f"Graceful shutdown failed, force killing PID: {old_pid}")
                        process.kill()
                        process.wait()
                        logger.info(f"Force killed existing instance (PID: {old_pid})")

                    return True
                else:
                    logger.debug(f"PID {old_pid} exists but is not a token refresher process")

            except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
                logger.debug(f"Could not access process {old_pid}: {e}")
        else:
            logger.debug(f"PID {old_pid} no longer exists")

        # Clean up stale PID file
        _remove_pidfile()
        return False

    except (ValueError, FileNotFoundError) as e:
        logger.debug(f"Invalid or missing PID file: {e}")
        _remove_pidfile()
        return False
    except Exception as e:
        logger.error(f"Error checking for existing instance: {e}")
        return False


def _setup_signal_handlers() -> None:
    """
    Set up signal handlers for graceful shutdown.
    """

    def signal_handler(signum, frame):
        logger.info(f"Received signal {signum}, shutting down gracefully...")
        _remove_pidfile()
        sys.exit(0)

    signal.signal(signal.SIGTERM, signal_handler)
    signal.signal(signal.SIGINT, signal_handler)


def main():
    """Main entry point for the token refresher service."""
    parser = argparse.ArgumentParser(
        description="OAuth Token Refresher Service",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
    # Run with default settings (check every 5 minutes, refresh 1 hour before expiry)
    uv run python credentials-provider/token_refresher.py
    
    # Check every 10 minutes
    uv run python credentials-provider/token_refresher.py --interval 600
    
    # Refresh tokens 2 hours before expiry
    uv run python credentials-provider/token_refresher.py --buffer 7200
    
    # Run once and exit (for testing)
    uv run python credentials-provider/token_refresher.py --once
    
    # Force refresh all tokens once and exit
    uv run python credentials-provider/token_refresher.py --once --force
    
    # Run in background with logging
    nohup uv run python credentials-provider/token_refresher.py > token_refresher.log 2>&1 &
""",
    )

    parser.add_argument(
        "--interval",
        type=int,
        default=DEFAULT_CHECK_INTERVAL,
        help=f"Check interval in seconds (default: {DEFAULT_CHECK_INTERVAL})",
    )

    parser.add_argument(
        "--buffer",
        type=int,
        default=DEFAULT_EXPIRY_BUFFER,
        help=f"Refresh tokens this many seconds before expiry (default: {DEFAULT_EXPIRY_BUFFER})",
    )

    parser.add_argument("--once", action="store_true", help="Run once and exit (for testing)")

    parser.add_argument(
        "--force",
        action="store_true",
        help="Force refresh all tokens regardless of expiration status",
    )

    parser.add_argument("--debug", action="store_true", help="Enable debug logging")

    parser.add_argument(
        "--no-kill",
        action="store_true",
        help="Do not kill existing instance (will exit if one is running)",
    )

    args = parser.parse_args()

    # Load environment variables from .env file
    _load_env_file()

    # Set debug logging if requested
    if args.debug:
        logging.getLogger().setLevel(logging.DEBUG)

    # Handle existing instances
    if not args.once:  # Only check for existing instances in continuous mode
        if args.no_kill:
            pidfile = _get_pidfile_path()
            if pidfile.exists():
                try:
                    with open(pidfile) as f:
                        existing_pid = int(f.read().strip())
                    if psutil.pid_exists(existing_pid):
                        logger.error(
                            f"Another token refresher instance is already running (PID: {existing_pid})"
                        )
                        logger.error(
                            "Use --no-kill flag to prevent automatic killing, or stop the existing instance first"
                        )
                        sys.exit(1)
                except Exception as e:
                    logger.debug(f"Invalid PID file, continuing: {e}")
        else:
            # Kill existing instance if found
            killed = _kill_existing_instance()
            if killed:
                logger.info("Existing instance terminated, starting new instance")
                time.sleep(1)  # Brief pause to ensure cleanup

    logger.info("=" * 60)
    logger.info("OAuth Token Refresher Service Starting")
    logger.info(f"Check interval: {args.interval} seconds")
    logger.info(f"Expiry buffer: {args.buffer} seconds ({args.buffer / 3600:.1f} hours)")
    logger.info("OAuth tokens directory is configured")
    logger.info("=" * 60)

    # Set up signal handlers and PID file for continuous mode
    if not args.once:
        _setup_signal_handlers()
        _write_pidfile()

    try:
        # Run once or continuously
        if args.once:
            logger.info("Running single refresh cycle...")
            _run_refresh_cycle(args.buffer, args.force)
        else:
            logger.info("Starting continuous monitoring...")
            while True:
                try:
                    _run_refresh_cycle(args.buffer, args.force)
                    logger.info(f"Sleeping for {args.interval} seconds...")
                    time.sleep(args.interval)
                except KeyboardInterrupt:
                    logger.info("Received interrupt signal, shutting down...")
                    break
                except Exception as e:
                    logger.error(f"Unexpected error in refresh cycle: {e}")
                    logger.info(f"Continuing after error, sleeping for {args.interval} seconds...")
                    time.sleep(args.interval)
    finally:
        # Clean up PID file
        if not args.once:
            _remove_pidfile()

    logger.info("Token Refresher Service stopped")


if __name__ == "__main__":
    main()
