"""Utilities for loading skills for V1 conversations.

This module provides functions to load skills from the agent-server,
which centralizes all skill loading logic. The app-server acts as a
thin proxy that:
1. Builds the org_config with authentication information
2. Builds the sandbox_config with exposed URLs
3. Calls the agent-server's /api/skills endpoint

All source-specific skill loading is handled by the agent-server.
"""

import logging

import httpx
from pydantic import BaseModel

from openhands.app_server.integrations.provider import ProviderType
from openhands.app_server.integrations.service_types import AuthenticationError
from openhands.app_server.sandbox.sandbox_models import SandboxInfo
from openhands.app_server.user.user_context import UserContext
from openhands.sdk.skills import KeywordTrigger, Skill, TaskTrigger

_logger = logging.getLogger(__name__)


class ExposedUrlConfig(BaseModel):
    """Configuration for an exposed URL in sandbox config."""

    name: str
    url: str
    port: int


class SandboxConfig(BaseModel):
    """Sandbox configuration for agent-server API request."""

    exposed_urls: list[ExposedUrlConfig]


class OrgConfig(BaseModel):
    """Organization configuration for agent-server API request."""

    repository: str
    provider: str
    org_repo_url: str
    org_name: str


class SkillInfo(BaseModel):
    """Skill information from agent-server API response."""

    name: str
    content: str
    triggers: list[str] = []
    source: str | None = None
    description: str | None = None
    is_agentskills_format: bool = False


async def _is_gitlab_repository(repo_name: str, user_context: UserContext) -> bool:
    """Check if a repository is hosted on GitLab.

    Args:
        repo_name: Repository name (e.g., "gitlab.com/org/repo" or "org/repo")
        user_context: UserContext to access provider handler

    Returns:
        True if the repository is hosted on GitLab, False otherwise
    """
    try:
        provider_handler = await user_context.get_provider_handler()  # type: ignore[attr-defined]
        repository = await provider_handler.verify_repo_provider(
            repo_name, is_optional=True
        )
        return repository.git_provider == ProviderType.GITLAB
    except Exception:
        return False


async def _is_azure_devops_repository(
    repo_name: str, user_context: UserContext
) -> bool:
    """Check if a repository is hosted on Azure DevOps.

    Args:
        repo_name: Repository name (e.g., "org/project/repo")
        user_context: UserContext to access provider handler

    Returns:
        True if the repository is hosted on Azure DevOps, False otherwise
    """
    try:
        provider_handler = await user_context.get_provider_handler()  # type: ignore[attr-defined]
        repository = await provider_handler.verify_repo_provider(
            repo_name, is_optional=True
        )
        return repository.git_provider == ProviderType.AZURE_DEVOPS
    except Exception:
        return False


async def _get_provider_type(
    selected_repository: str, user_context: UserContext
) -> str:
    """Determine the Git provider type for a repository.

    Args:
        selected_repository: Repository name (e.g., 'owner/repo')
        user_context: UserContext to access provider handler

    Returns:
        Provider type string: 'github', 'gitlab', 'azure', or 'bitbucket'
    """
    is_gitlab = await _is_gitlab_repository(selected_repository, user_context)
    if is_gitlab:
        return 'gitlab'

    is_azure = await _is_azure_devops_repository(selected_repository, user_context)
    if is_azure:
        return 'azure'

    # Default to github (covers github and bitbucket)
    return 'github'


async def _determine_org_repo_path(
    selected_repository: str, user_context: UserContext
) -> tuple[str, str]:
    """Determine the organization repository path and organization name.

    Args:
        selected_repository: Repository name (e.g., 'owner/repo' or 'org/project/repo')
        user_context: UserContext to access provider handler

    Returns:
        Tuple of (org_repo_path, org_name) where:
        - org_repo_path: Full path to org-level config repo
        - org_name: Organization name extracted from repository

    Examples:
        - GitHub/Bitbucket: ('owner/.openhands', 'owner')
        - GitLab: ('owner/openhands-config', 'owner')
        - Azure DevOps: ('org/openhands-config/openhands-config', 'org')
    """
    repo_parts = selected_repository.split('/')

    is_azure_devops = await _is_azure_devops_repository(
        selected_repository, user_context
    )
    is_gitlab = await _is_gitlab_repository(selected_repository, user_context)

    if is_azure_devops and len(repo_parts) >= 3:
        org_name = repo_parts[0]
    else:
        org_name = repo_parts[-2]

    if is_gitlab:
        org_openhands_repo = f'{org_name}/openhands-config'
    elif is_azure_devops:
        org_openhands_repo = f'{org_name}/openhands-config/openhands-config'
    else:
        org_openhands_repo = f'{org_name}/.openhands'

    return org_openhands_repo, org_name


async def _get_org_repository_url(
    org_openhands_repo: str, user_context: UserContext
) -> str | None:
    """Get authenticated Git URL for organization repository.

    Args:
        org_openhands_repo: Organization repository path
        user_context: UserContext to access authentication

    Returns:
        Authenticated Git URL if successful, None otherwise
    """
    try:
        remote_url = await user_context.get_authenticated_git_url(
            org_openhands_repo, is_optional=True
        )
        return remote_url
    except AuthenticationError as e:
        _logger.debug(
            f'org-level skill directory {org_openhands_repo} not found: {str(e)}'
        )
        return None
    except Exception as e:
        _logger.debug(
            f'Failed to get authenticated URL for {org_openhands_repo}: {str(e)}'
        )
        return None


async def build_org_config(
    selected_repository: str | None,
    user_context: UserContext,
) -> OrgConfig | None:
    """Build organization config for agent-server API request.

    Args:
        selected_repository: Repository name (e.g., 'owner/repo') or None
        user_context: UserContext to access authentication and provider info

    Returns:
        org_config dict if org repository exists and is accessible, None otherwise
    """
    if not selected_repository:
        return None

    repo_parts = selected_repository.split('/')
    if len(repo_parts) < 2:
        _logger.warning(
            f'Repository path has insufficient parts ({len(repo_parts)} < 2), '
            f'skipping org-level skills'
        )
        return None

    try:
        org_openhands_repo, org_name = await _determine_org_repo_path(
            selected_repository, user_context
        )

        org_repo_url = await _get_org_repository_url(org_openhands_repo, user_context)
        if not org_repo_url:
            return None

        provider = await _get_provider_type(selected_repository, user_context)

        return OrgConfig(
            repository=selected_repository,
            provider=provider,
            org_repo_url=org_repo_url,
            org_name=org_name,
        )

    except Exception as e:
        _logger.debug(f'Failed to build org config: {str(e)}')
        return None


def build_sandbox_config(sandbox: SandboxInfo) -> SandboxConfig | None:
    """Build sandbox config for agent-server API request.

    Args:
        sandbox: SandboxInfo containing exposed URLs

    Returns:
        sandbox_config dict if there are exposed URLs, None otherwise
    """
    if not sandbox.exposed_urls:
        return None

    exposed_urls = [
        ExposedUrlConfig(name=url.name, url=url.url, port=url.port)
        for url in sandbox.exposed_urls
    ]

    return SandboxConfig(exposed_urls=exposed_urls)


async def load_skills_from_agent_server(
    agent_server_url: str,
    session_api_key: str | None,
    project_dir: str,
    org_config: OrgConfig | None = None,
    sandbox_config: SandboxConfig | None = None,
    load_public: bool = True,
    load_user: bool = True,
    load_project: bool = True,
    load_org: bool = True,
) -> list[Skill]:
    """Load all skills from the agent-server.

    This function makes a single API call to the agent-server's /api/skills
    endpoint to load and merge skills from all configured sources.

    Args:
        agent_server_url: URL of the agent server (e.g., 'http://localhost:8000')
        session_api_key: Session API key for authentication (optional)
        project_dir: Workspace directory path for project skills
        org_config: Organization skills configuration (optional)
        sandbox_config: Sandbox skills configuration (optional)
        load_public: Whether to load public skills (default: True)
        load_user: Whether to load user skills (default: True)
        load_project: Whether to load project skills (default: True)
        load_org: Whether to load organization skills (default: True)

    Returns:
        List of Skill objects merged from all sources.
        Returns empty list on error.
    """
    try:
        # Build request payload
        payload = {
            'load_public': load_public,
            'load_user': load_user,
            'load_project': load_project,
            'load_org': load_org,
            'project_dir': project_dir,
            'org_config': org_config.model_dump() if org_config else None,
            'sandbox_config': sandbox_config.model_dump() if sandbox_config else None,
        }

        # Build headers
        headers = {'Content-Type': 'application/json'}
        if session_api_key:
            headers['X-Session-API-Key'] = session_api_key

        # Make API request
        async with httpx.AsyncClient() as client:
            response = await client.post(
                f'{agent_server_url}/api/skills',
                json=payload,
                headers=headers,
                timeout=60.0,
            )
            response.raise_for_status()

            data = response.json()

        # Convert response to Skill objects
        skills: list[Skill] = []
        for skill_data_dict in data.get('skills', []):
            try:
                skill_info = SkillInfo.model_validate(skill_data_dict)
                skill = _convert_skill_info_to_skill(skill_info)
                skills.append(skill)
            except Exception as e:
                skill_name = (
                    skill_data_dict.get('name', 'unknown')
                    if isinstance(skill_data_dict, dict)
                    else 'unknown'
                )
                _logger.warning(f'Failed to convert skill {skill_name}: {e}')

        sources = data.get('sources', {})
        _logger.info(
            f'Loaded {len(skills)} skills from agent-server: '
            f'sources={sources}, names={[s.name for s in skills]}'
        )

        return skills

    except httpx.HTTPStatusError as e:
        _logger.warning(
            f'Agent-server returned error status {e.response.status_code}: '
            f'{e.response.text}'
        )
        return []
    except httpx.RequestError as e:
        _logger.warning(f'Failed to connect to agent-server: {e}')
        return []
    except Exception as e:
        _logger.warning(f'Failed to load skills from agent-server: {e}')
        return []


def _convert_skill_info_to_skill(skill_info: SkillInfo) -> Skill:
    """Convert skill info from API response to Skill object.

    Args:
        skill_info: SkillInfo model from API response

    Returns:
        Skill object
    """
    trigger: TaskTrigger | KeywordTrigger | None = None

    if skill_info.triggers:
        # Determine trigger type based on content
        if any(t.startswith('/') for t in skill_info.triggers):
            trigger = TaskTrigger(triggers=skill_info.triggers)
        else:
            trigger = KeywordTrigger(keywords=skill_info.triggers)

    return Skill(
        name=skill_info.name,
        content=skill_info.content,
        trigger=trigger,
        source=skill_info.source,
        description=skill_info.description,
        is_agentskills_format=skill_info.is_agentskills_format,
    )
