"""Configuration for the OpenHands App Server."""

import os
from pathlib import Path
from typing import AsyncContextManager

import httpx
from fastapi import Depends, Request
from pydantic import Field, SecretStr

# Import the event_callback module to ensure all processors are registered
import openhands.app_server.event_callback  # noqa: F401
from openhands.agent_server.env_parser import from_env
from openhands.app_server.app_conversation.app_conversation_info_service import (
    AppConversationInfoService,
    AppConversationInfoServiceInjector,
)
from openhands.app_server.app_conversation.app_conversation_service import (
    AppConversationService,
    AppConversationServiceInjector,
)
from openhands.app_server.app_conversation.app_conversation_start_task_service import (
    AppConversationStartTaskService,
    AppConversationStartTaskServiceInjector,
)
from openhands.app_server.app_lifespan.app_lifespan_service import AppLifespanService
from openhands.app_server.app_lifespan.oss_app_lifespan_service import (
    OssAppLifespanService,
)
from openhands.app_server.config_api.config_models import AppMode
from openhands.app_server.config_api.llm_model_service import (
    LLMModelService,
    LLMModelServiceInjector,
)
from openhands.app_server.event.event_service import EventService, EventServiceInjector
from openhands.app_server.event_callback.event_callback_service import (
    EventCallbackService,
    EventCallbackServiceInjector,
)
from openhands.app_server.file_store.files import FileStore
from openhands.app_server.file_store.local import LocalFileStore
from openhands.app_server.pending_messages.pending_message_service import (
    PendingMessageService,
    PendingMessageServiceInjector,
)
from openhands.app_server.sandbox.sandbox_service import (
    SandboxService,
    SandboxServiceInjector,
)
from openhands.app_server.sandbox.sandbox_spec_service import (
    SandboxSpecService,
    SandboxSpecServiceInjector,
)
from openhands.app_server.services.db_session import (  # noqa: F401  (re-exported)
    depends_db_session,
    get_db_session,
)
from openhands.app_server.services.db_session_injector import (
    DbSessionInjector,
)
from openhands.app_server.services.httpx_client_injector import HttpxClientInjector
from openhands.app_server.services.injector import InjectorState
from openhands.app_server.services.jwt_service import JwtService, JwtServiceInjector
from openhands.app_server.user.user_context import UserContext, UserContextInjector
from openhands.app_server.utils.environment import StorageProvider, get_storage_provider
from openhands.app_server.web_client.default_web_client_config_injector import (
    DefaultWebClientConfigInjector,
)
from openhands.app_server.web_client.web_client_config_injector import (
    WebClientConfigInjector,
)
from openhands.sdk.utils.models import OpenHandsModel


def get_default_persistence_dir() -> Path:
    # Recheck env because this function is also used to generate other defaults
    persistence_dir = os.getenv('OH_PERSISTENCE_DIR')

    # Legacy V0 fallback variable
    if persistence_dir is None:
        persistence_dir = os.getenv('FILE_STORE_PATH')

    if persistence_dir:
        result = Path(persistence_dir)
    else:
        result = Path.home() / '.openhands'

    result.mkdir(parents=True, exist_ok=True)
    return result


def get_default_web_url() -> str | None:
    """Get legacy web host parameter.

    If present, we assume we are running under https.
    """
    web_host = os.getenv('WEB_HOST')
    if not web_host:
        return None
    return f'https://{web_host}'


def get_default_permitted_cors_origins() -> list[str]:
    """Get permitted CORS origins, falling back to legacy PERMITTED_CORS_ORIGINS env var.

    The preferred configuration is via OH_PERMITTED_CORS_ORIGINS_0, _1, etc.
    (handled by the pydantic from_env parser). This fallback supports the legacy
    comma-separated PERMITTED_CORS_ORIGINS environment variable.
    """
    legacy = os.getenv('PERMITTED_CORS_ORIGINS', '')
    if legacy:
        return [o.strip() for o in legacy.split(',') if o.strip()]
    return []


def get_openhands_provider_base_url() -> str | None:
    """Return the base URL for the OpenHands provider, if configured.

    Falls back to LLM_BASE_URL for backward compatibility.
    """
    return os.getenv('OPENHANDS_PROVIDER_BASE_URL') or os.getenv('LLM_BASE_URL') or None


def get_default_tavily_api_key() -> str | None:
    """Return the Tavily API key from environment, if configured.

    Falls back to SEARCH_API_KEY for backward compatibility.
    """
    return os.getenv('TAVILY_API_KEY') or os.getenv('SEARCH_API_KEY') or None


# The SDK auto-fills this URL as the default for openhands/ and litellm_proxy/
# models.  Deployments (e.g. staging) may use a different LLM proxy, configured
# via OPENHANDS_PROVIDER_BASE_URL.
_SDK_DEFAULT_PROXY = 'https://llm-proxy.app.all-hands.dev/'


def resolve_provider_llm_base_url(
    model: str | None,
    base_url: str | None,
    provider_base_url: str | None = None,
) -> str | None:
    """Apply deployment-specific LLM proxy override when needed.

    When the model uses ``openhands/`` or ``litellm_proxy/`` prefix and the
    stored ``base_url`` is the SDK default, replace it with the deployment's
    provider URL.

    Priority: user-explicit URL > deployment provider URL > SDK default.

    Args:
        model: LLM model name (e.g. ``litellm_proxy/gpt-4``).
        base_url: The base URL from user/org settings.
        provider_base_url: Deployment provider URL.  Falls back to
            ``get_openhands_provider_base_url()`` when *None*.
    """
    if not model or not (
        model.startswith('openhands/') or model.startswith('litellm_proxy/')
    ):
        return base_url

    user_set_custom = base_url and base_url.rstrip('/') != _SDK_DEFAULT_PROXY.rstrip(
        '/'
    )
    if user_set_custom:
        return base_url

    if provider_base_url is None:
        provider_base_url = get_openhands_provider_base_url()
    if provider_base_url:
        return provider_base_url

    return base_url


def _get_default_lifespan():
    # Check legacy parameters for saas mode. If we are in SAAS mode use
    # SaasAppLifespanService to initialize PostHog analytics
    if 'saas' in (os.getenv('OPENHANDS_CONFIG_CLS') or '').lower():
        from server.app_lifespan.saas_app_lifespan_service import (
            SaasAppLifespanService,
        )

        return SaasAppLifespanService()
    return OssAppLifespanService()


def _get_default_file_store() -> FileStore:
    """Create a default LocalFileStore using the default persistence directory."""
    return LocalFileStore(root=str(get_default_persistence_dir()))


class AppServerConfig(OpenHandsModel):
    persistence_dir: Path = Field(default_factory=get_default_persistence_dir)
    file_store: FileStore = Field(default_factory=_get_default_file_store)
    web_url: str | None = Field(
        default_factory=get_default_web_url,
        description='The URL where OpenHands is running (e.g., http://localhost:3000)',
    )
    permitted_cors_origins: list[str] = Field(
        default_factory=get_default_permitted_cors_origins,
        description=(
            'Additional permitted CORS origins for both the app server and agent '
            'server containers. Configure via OH_PERMITTED_CORS_ORIGINS_0, _1, etc. '
            'Falls back to legacy PERMITTED_CORS_ORIGINS env var.'
        ),
    )
    openhands_provider_base_url: str | None = Field(
        default_factory=get_openhands_provider_base_url,
        description='Base URL for the OpenHands provider',
    )
    tavily_api_key: str | None = Field(
        default_factory=get_default_tavily_api_key,
        description='Tavily API key for search integration (proxied via MCP server)',
    )
    # Dependency Injection Injectors
    llm_model: LLMModelServiceInjector | None = None
    event: EventServiceInjector | None = None
    event_callback: EventCallbackServiceInjector | None = None
    sandbox: SandboxServiceInjector | None = None
    sandbox_spec: SandboxSpecServiceInjector | None = None
    app_conversation_info: AppConversationInfoServiceInjector | None = None
    app_conversation_start_task: AppConversationStartTaskServiceInjector | None = None
    app_conversation: AppConversationServiceInjector | None = None
    pending_message: PendingMessageServiceInjector | None = None
    user: UserContextInjector | None = None
    jwt: JwtServiceInjector | None = None
    httpx: HttpxClientInjector = Field(default_factory=HttpxClientInjector)
    db_session: DbSessionInjector = Field(
        default_factory=lambda: DbSessionInjector(
            persistence_dir=get_default_persistence_dir()
        )
    )
    # Services
    lifespan: AppLifespanService | None = Field(default_factory=_get_default_lifespan)
    app_mode: AppMode = AppMode.OPENHANDS
    web_client: WebClientConfigInjector = Field(
        default_factory=DefaultWebClientConfigInjector
    )


def config_from_env() -> AppServerConfig:
    # Import defaults...
    from openhands.app_server.app_conversation.live_status_app_conversation_service import (  # noqa: E501
        LiveStatusAppConversationServiceInjector,
    )
    from openhands.app_server.app_conversation.sql_app_conversation_info_service import (  # noqa: E501
        SQLAppConversationInfoServiceInjector,
    )
    from openhands.app_server.app_conversation.sql_app_conversation_start_task_service import (  # noqa: E501
        SQLAppConversationStartTaskServiceInjector,
    )
    from openhands.app_server.event.aws_event_service import (
        AwsEventServiceInjector,
    )
    from openhands.app_server.event.filesystem_event_service import (
        FilesystemEventServiceInjector,
    )
    from openhands.app_server.event.google_cloud_event_service import (
        GoogleCloudEventServiceInjector,
    )
    from openhands.app_server.event_callback.sql_event_callback_service import (
        SQLEventCallbackServiceInjector,
    )
    from openhands.app_server.sandbox.docker_sandbox_service import (
        DockerSandboxServiceInjector,
    )
    from openhands.app_server.sandbox.docker_sandbox_spec_service import (
        DockerSandboxSpecServiceInjector,
    )
    from openhands.app_server.sandbox.process_sandbox_service import (
        ProcessSandboxServiceInjector,
    )
    from openhands.app_server.sandbox.process_sandbox_spec_service import (
        ProcessSandboxSpecServiceInjector,
    )
    from openhands.app_server.sandbox.remote_sandbox_service import (
        RemoteSandboxServiceInjector,
    )
    from openhands.app_server.sandbox.remote_sandbox_spec_service import (
        RemoteSandboxSpecServiceInjector,
    )
    from openhands.app_server.user.auth_user_context import (
        AuthUserContextInjector,
    )

    config: AppServerConfig = from_env(AppServerConfig, 'OH')  # type: ignore

    if config.llm_model is None:
        from openhands.app_server.config_api.default_llm_model_service import (
            DefaultLLMModelServiceInjector,
        )

        llm_model_kwargs: dict = {}
        aws_region = os.getenv('AWS_REGION_NAME')
        aws_key = os.getenv('AWS_ACCESS_KEY_ID')
        aws_secret = os.getenv('AWS_SECRET_ACCESS_KEY')
        if aws_region and aws_key and aws_secret:
            llm_model_kwargs['aws_region_name'] = aws_region
            llm_model_kwargs['aws_access_key_id'] = SecretStr(aws_key)
            llm_model_kwargs['aws_secret_access_key'] = SecretStr(aws_secret)

        ollama_url = os.getenv('OLLAMA_BASE_URL')
        if ollama_url:
            llm_model_kwargs['ollama_base_url'] = ollama_url

        config.llm_model = DefaultLLMModelServiceInjector(**llm_model_kwargs)

    if config.event is None:
        provider = get_storage_provider()

        if provider == StorageProvider.AWS:
            # AWS S3 storage configuration
            bucket_name = os.environ.get('FILE_STORE_PATH')
            if not bucket_name:
                raise ValueError(
                    'FILE_STORE_PATH environment variable is required for S3 storage'
                )
            config.event = AwsEventServiceInjector(bucket_name=bucket_name)
        elif provider == StorageProvider.GCP:
            # Google Cloud storage configuration
            bucket_name = os.environ.get('FILE_STORE_PATH')
            if not bucket_name:
                raise ValueError(
                    'FILE_STORE_PATH environment variable is required for Google Cloud storage'
                )
            config.event = GoogleCloudEventServiceInjector(bucket_name=bucket_name)
        else:
            config.event = FilesystemEventServiceInjector()

    if config.event_callback is None:
        config.event_callback = SQLEventCallbackServiceInjector()

    if config.sandbox is None:
        # Legacy fallback
        if os.getenv('RUNTIME') == 'remote':
            config.sandbox = RemoteSandboxServiceInjector(
                api_key=os.environ['SANDBOX_API_KEY'],
                api_url=os.environ['SANDBOX_REMOTE_RUNTIME_API_URL'],
            )
        elif os.getenv('RUNTIME') in ('local', 'process'):
            config.sandbox = ProcessSandboxServiceInjector()
        else:
            # Support legacy environment variables for Docker sandbox configuration
            docker_sandbox_kwargs: dict = {}
            if os.getenv('SANDBOX_HOST_PORT'):
                docker_sandbox_kwargs['host_port'] = int(
                    os.environ['SANDBOX_HOST_PORT']
                )
            if os.getenv('SANDBOX_CONTAINER_URL_PATTERN'):
                docker_sandbox_kwargs['container_url_pattern'] = os.environ[
                    'SANDBOX_CONTAINER_URL_PATTERN'
                ]
            # Allow configuring sandbox startup grace period
            # This is useful for slower machines or cloud environments where
            # the agent-server container takes longer to initialize
            if os.getenv('SANDBOX_STARTUP_GRACE_SECONDS'):
                docker_sandbox_kwargs['startup_grace_seconds'] = int(
                    os.environ['SANDBOX_STARTUP_GRACE_SECONDS']
                )
            # Parse SANDBOX_VOLUMES and convert to VolumeMount objects
            # This is set by the CLI's --mount-cwd flag
            sandbox_volumes = os.getenv('SANDBOX_VOLUMES')
            if sandbox_volumes:
                from openhands.app_server.sandbox.docker_sandbox_service import (
                    VolumeMount,
                )

                mounts = []
                for mount_spec in sandbox_volumes.split(','):
                    mount_spec = mount_spec.strip()
                    if not mount_spec:
                        continue
                    parts = mount_spec.split(':')
                    if len(parts) >= 2:
                        host_path = parts[0]
                        container_path = parts[1]
                        mode = parts[2] if len(parts) > 2 else 'rw'
                        mounts.append(
                            VolumeMount(
                                host_path=host_path,
                                container_path=container_path,
                                mode=mode,
                            )
                        )
                if mounts:
                    docker_sandbox_kwargs['mounts'] = mounts
            config.sandbox = DockerSandboxServiceInjector(**docker_sandbox_kwargs)

    if config.sandbox_spec is None:
        if os.getenv('RUNTIME') == 'remote':
            config.sandbox_spec = RemoteSandboxSpecServiceInjector()
        elif os.getenv('RUNTIME') in ('local', 'process'):
            config.sandbox_spec = ProcessSandboxSpecServiceInjector()
        else:
            config.sandbox_spec = DockerSandboxSpecServiceInjector()

    if config.app_conversation_info is None:
        config.app_conversation_info = SQLAppConversationInfoServiceInjector()

    if config.app_conversation_start_task is None:
        config.app_conversation_start_task = (
            SQLAppConversationStartTaskServiceInjector()
        )

    if config.app_conversation is None:
        config.app_conversation = LiveStatusAppConversationServiceInjector()

    if config.pending_message is None:
        from openhands.app_server.pending_messages.pending_message_service import (
            SQLPendingMessageServiceInjector,
        )

        config.pending_message = SQLPendingMessageServiceInjector()

    if config.user is None:
        config.user = AuthUserContextInjector()

    if config.jwt is None:
        config.jwt = JwtServiceInjector(persistence_dir=config.persistence_dir)

    return config


_global_config: AppServerConfig | None = None


def get_global_config() -> AppServerConfig:
    """Get the default local server config shared across the server."""
    global _global_config
    if _global_config is None:
        # Load configuration from environment...
        _global_config = config_from_env()

    return _global_config  # type: ignore


def get_event_service(
    state: InjectorState, request: Request | None = None
) -> AsyncContextManager[EventService]:
    injector = get_global_config().event
    assert injector is not None
    return injector.context(state, request)


def get_event_callback_service(
    state: InjectorState, request: Request | None = None
) -> AsyncContextManager[EventCallbackService]:
    injector = get_global_config().event_callback
    assert injector is not None
    return injector.context(state, request)


def get_sandbox_service(
    state: InjectorState, request: Request | None = None
) -> AsyncContextManager[SandboxService]:
    injector = get_global_config().sandbox
    assert injector is not None
    return injector.context(state, request)


def get_sandbox_spec_service(
    state: InjectorState, request: Request | None = None
) -> AsyncContextManager[SandboxSpecService]:
    injector = get_global_config().sandbox_spec
    assert injector is not None
    return injector.context(state, request)


def get_app_conversation_info_service(
    state: InjectorState, request: Request | None = None
) -> AsyncContextManager[AppConversationInfoService]:
    injector = get_global_config().app_conversation_info
    assert injector is not None
    return injector.context(state, request)


def get_app_conversation_start_task_service(
    state: InjectorState, request: Request | None = None
) -> AsyncContextManager[AppConversationStartTaskService]:
    injector = get_global_config().app_conversation_start_task
    assert injector is not None
    return injector.context(state, request)


def get_app_conversation_service(
    state: InjectorState, request: Request | None = None
) -> AsyncContextManager[AppConversationService]:
    injector = get_global_config().app_conversation
    assert injector is not None
    return injector.context(state, request)


def get_pending_message_service(
    state: InjectorState, request: Request | None = None
) -> AsyncContextManager[PendingMessageService]:
    injector = get_global_config().pending_message
    assert injector is not None
    return injector.context(state, request)


def get_user_context(
    state: InjectorState, request: Request | None = None
) -> AsyncContextManager[UserContext]:
    injector = get_global_config().user
    assert injector is not None
    return injector.context(state, request)


def get_httpx_client(
    state: InjectorState, request: Request | None = None
) -> AsyncContextManager[httpx.AsyncClient]:
    return get_global_config().httpx.context(state, request)


def get_jwt_service(
    state: InjectorState, request: Request | None = None
) -> AsyncContextManager[JwtService]:
    injector = get_global_config().jwt
    assert injector is not None
    return injector.context(state, request)


def get_app_lifespan_service() -> AppLifespanService | None:
    config = get_global_config()
    return config.lifespan


def depends_event_service():
    injector = get_global_config().event
    assert injector is not None
    return Depends(injector.depends)


def depends_event_callback_service():
    injector = get_global_config().event_callback
    assert injector is not None
    return Depends(injector.depends)


def depends_sandbox_service():
    injector = get_global_config().sandbox
    assert injector is not None
    return Depends(injector.depends)


def depends_sandbox_spec_service():
    injector = get_global_config().sandbox_spec
    assert injector is not None
    return Depends(injector.depends)


def depends_app_conversation_info_service():
    injector = get_global_config().app_conversation_info
    assert injector is not None
    return Depends(injector.depends)


def depends_app_conversation_start_task_service():
    injector = get_global_config().app_conversation_start_task
    assert injector is not None
    return Depends(injector.depends)


def depends_app_conversation_service():
    injector = get_global_config().app_conversation
    assert injector is not None
    return Depends(injector.depends)


def depends_pending_message_service():
    injector = get_global_config().pending_message
    assert injector is not None
    return Depends(injector.depends)


def depends_user_context():
    injector = get_global_config().user
    assert injector is not None
    return Depends(injector.depends)


def depends_httpx_client():
    return Depends(get_global_config().httpx.depends)


def depends_jwt_service():
    injector = get_global_config().jwt
    assert injector is not None
    return Depends(injector.depends)


def get_llm_model_service(
    state: InjectorState, request: Request | None = None
) -> AsyncContextManager[LLMModelService]:
    injector = get_global_config().llm_model
    assert injector is not None
    return injector.context(state, request)


def depends_llm_model_service():
    injector = get_global_config().llm_model
    assert injector is not None
    return Depends(injector.depends)
