"""Config profile API endpoints."""

import logging
import os
import subprocess
import uuid
from typing import Any

from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel, Field, field_validator, model_validator
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload

from src.api.shared_validators import validate_env_vars as _validate_env_vars
from src.auth.dependencies import get_current_user_id, get_db_session
from src.models.config_profile import ConfigProfile, ConfigProfileInclude
from src.models.project import Project
from src.models.tool_type import ToolType
from src.services.config_profile_resolver import (
    ConfigProfileCycleError,
    check_include_cycle,
    resolve_profile,
    resolved_profile_to_dict,
)
from src.utils.git_url_parser import parse_git_url

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/config-profiles", tags=["config-profiles"])

MAX_PROFILE_SIZE_MB = 10
MAX_PROFILE_SIZE_BYTES = MAX_PROFILE_SIZE_MB * 1024 * 1024


def _validate_uuid(v: str | None) -> str | None:
    if v is None:
        return v
    try:
        uuid.UUID(v)
    except ValueError:
        raise ValueError(f"Invalid UUID: {v}")
    return v


def _calculate_profile_size(data: dict) -> int:
    """Calculate approximate serialized size of profile data."""
    total = 0
    for key, value in data.get("env_vars", {}).items():
        total += len(key.encode("utf-8")) + len(str(value).encode("utf-8"))
    for key, value in data.get("runtime_hints", {}).items():
        total += len(key.encode("utf-8")) + len(str(value).encode("utf-8"))
    for mount in data.get("mounts", []):
        total += len(str(mount.get("target", "")).encode("utf-8"))
        total += len(str(mount.get("mode", "")).encode("utf-8"))
        for path, content in mount.get("files", {}).items():
            total += len(path.encode("utf-8")) + len(content.encode("utf-8"))
    for path, content in data.get("files", {}).items():
        total += len(path.encode("utf-8")) + len(content.encode("utf-8"))
    return total


class GitMountMapping(BaseModel):
    source_path: str = Field(
        description="Path within repository (supports glob patterns)"
    )
    target_path: str = Field(description="Absolute path inside container")

    @field_validator("source_path")
    @classmethod
    def validate_source_path(cls, v: str) -> str:
        if v.startswith("/"):
            raise ValueError("source_path must be relative (no leading /)")
        if ".." in v:
            raise ValueError("source_path cannot contain path traversal (..)")
        return v

    @field_validator("target_path")
    @classmethod
    def validate_target_path(cls, v: str) -> str:
        if ".." in v:
            raise ValueError("target_path cannot contain path traversal (..)")
        return v


class GitMountItem(BaseModel):
    remote_url: str = Field(description="Git remote URL (HTTPS or SSH)")
    source_path: str | None = Field(
        default=None, description="Path within repository (legacy single mapping)"
    )
    target_path: str | None = Field(
        default=None,
        description="Absolute path inside container (legacy single mapping)",
    )
    branch: str | None = Field(default=None, description="Optional branch or tag name")
    mappings: list[GitMountMapping] | None = Field(
        default=None, description="Multiple source/target mappings from the same repo"
    )

    @field_validator("remote_url")
    @classmethod
    def validate_remote_url(cls, v: str) -> str:
        if not v.startswith(("http://", "https://", "git@", "ssh://")):
            raise ValueError(
                "remote_url must be a valid git URL (https://, git@, or ssh://)"
            )
        return v

    @field_validator("source_path")
    @classmethod
    def validate_source_path(cls, v: str | None) -> str | None:
        if v is None:
            return v
        if v.startswith("/"):
            raise ValueError("source_path must be relative (no leading /)")
        if ".." in v:
            raise ValueError("source_path cannot contain path traversal (..)")
        return v

    @field_validator("target_path")
    @classmethod
    def validate_target_path(cls, v: str | None) -> str | None:
        if v is None:
            return v
        if ".." in v:
            raise ValueError("target_path cannot contain path traversal (..)")
        return v

    @model_validator(mode="after")
    def check_mappings_or_legacy(self):
        has_legacy = self.source_path is not None and self.target_path is not None
        has_mappings = self.mappings is not None and len(self.mappings) > 0
        if not has_legacy and not has_mappings:
            raise ValueError(
                "Git mount must have either 'mappings' (non-empty array) or both 'source_path' and 'target_path'"
            )
        return self


class MountItem(BaseModel):
    target: str = Field(description="Absolute mount target path")
    mode: str = Field(default="rw", description="Mount mode: ro or rw")
    files: dict = Field(
        default_factory=dict, description="Files as {relative_path: content}"
    )

    @field_validator("target")
    @classmethod
    def validate_target(cls, v: str) -> str:
        if not v.startswith("/"):
            raise ValueError("Mount target must be absolute (start with /)")
        return v

    @field_validator("mode")
    @classmethod
    def validate_mode(cls, v: str) -> str:
        if v not in ("ro", "rw"):
            raise ValueError("Mount mode must be 'ro' or 'rw'")
        return v

    @field_validator("files")
    @classmethod
    def validate_files(cls, v: dict) -> dict:
        for path in v.keys():
            if ".." in path or not path:
                raise ValueError(f"Invalid file path: {path}")
            if path.startswith("/"):
                raise ValueError(
                    f"Mount file paths must be relative (got: {path}). "
                    f"The mount target defines the absolute container path."
                )
        return v


class ConfigProfileCreate(BaseModel):
    name: str = Field(description="Profile name (unique per user)")
    description: str | None = Field(default=None, description="Optional description")
    project_id: str | None = Field(default=None, description="Optional project ID")
    tool_type_id: str | None = Field(default=None, description="Optional tool type ID")
    env_vars: dict = Field(default_factory=dict, description="Environment variables")
    runtime_hints: dict = Field(default_factory=dict, description="Runtime hints")
    mounts: list[MountItem] = Field(
        default_factory=list, description="Mount definitions"
    )
    files: dict = Field(
        default_factory=dict, description="Files as {relative_path: content}"
    )
    git_mounts: list[GitMountItem] = Field(
        default_factory=list, description="Git repository mounts"
    )
    is_default: bool = Field(
        default=False, description="Whether this is the default profile for its scope"
    )

    @field_validator("project_id", "tool_type_id")
    @classmethod
    def validate_uuids(cls, v: str | None) -> str | None:
        return _validate_uuid(v)

    @field_validator("files")
    @classmethod
    def validate_files(cls, v: dict) -> dict:
        for path in v.keys():
            if ".." in path or not path:
                raise ValueError(f"Invalid file path: {path}")
            if path.startswith("/"):
                raise ValueError(
                    f"File paths must be relative (got: {path}). "
                    f"Use Mounts for absolute container paths."
                )
        return v

    @field_validator("env_vars")
    @classmethod
    def validate_env_vars(cls, v: dict) -> dict:
        result = _validate_env_vars(v)
        if result is None:
            raise ValueError("env_vars must be a JSON object")
        return result

    @field_validator("runtime_hints")
    @classmethod
    def validate_runtime_hints(cls, v: dict) -> dict:
        if not isinstance(v, dict):
            raise ValueError("runtime_hints must be a JSON object")
        return v

    @field_validator("mounts")
    @classmethod
    def validate_mounts(cls, v: list) -> list:
        if not isinstance(v, list):
            raise ValueError("mounts must be a JSON array")
        return v


class ConfigProfileUpdate(BaseModel):
    name: str | None = Field(default=None, description="Profile name")
    description: str | None = Field(default=None, description="Optional description")
    project_id: str | None = Field(default=None, description="Optional project ID")
    tool_type_id: str | None = Field(default=None, description="Optional tool type ID")
    env_vars: dict | None = Field(default=None, description="Environment variables")
    runtime_hints: dict | None = Field(default=None, description="Runtime hints")
    mounts: list[MountItem] | None = Field(
        default=None, description="Mount definitions"
    )
    files: dict | None = Field(
        default=None, description="Files as {relative_path: content}"
    )
    git_mounts: list[GitMountItem] | None = Field(
        default=None, description="Git repository mounts"
    )
    is_default: bool | None = Field(
        default=None, description="Whether this is the default profile"
    )

    @field_validator("project_id", "tool_type_id")
    @classmethod
    def validate_uuids(cls, v: str | None) -> str | None:
        return _validate_uuid(v)

    @field_validator("files")
    @classmethod
    def validate_files(cls, v: dict | None) -> dict | None:
        if v is None:
            return v
        for path in v.keys():
            if ".." in path or path.startswith("/") or not path:
                raise ValueError(f"Invalid file path: {path}")
        return v


class ConfigProfileIncludeUpdate(BaseModel):
    includes: list[str] = Field(description="Ordered list of included profile IDs")

    @field_validator("includes")
    @classmethod
    def validate_includes(cls, v: list) -> list:
        for item in v:
            try:
                uuid.UUID(item)
            except ValueError:
                raise ValueError(f"Invalid UUID in includes: {item}")
        return v


class ConfigProfileResponse(BaseModel):
    id: str
    user_id: str
    name: str
    description: str | None
    project_id: str | None
    tool_type_id: str | None
    env_vars: dict
    runtime_hints: dict
    mounts: list
    files: dict
    git_mounts: list
    is_default: bool
    includes: list[dict]
    created_at: str
    updated_at: str


async def _get_profile_with_includes(
    session: AsyncSession, profile_id: uuid.UUID
) -> ConfigProfile | None:
    """Fetch a profile with includes eagerly loaded."""
    result = await session.execute(
        select(ConfigProfile)
        .where(ConfigProfile.id == profile_id)
        .options(selectinload(ConfigProfile.includes))
    )
    return result.scalar_one_or_none()


async def _check_access(
    session: AsyncSession,
    user_id: uuid.UUID,
    project_id: uuid.UUID | None = None,
    tool_type_id: uuid.UUID | None = None,
) -> None:
    """Verify user has access to referenced project and tool type."""
    if project_id is not None:
        project = await session.get(Project, project_id)
        if project is None:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
            )
        # Add ownership check if needed; for now just verify existence
    if tool_type_id is not None:
        tool_type = await session.get(ToolType, tool_type_id)
        if tool_type is None:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND, detail="Tool type not found"
            )


async def _validate_git_mounts(
    session: AsyncSession,
    user_id: uuid.UUID,
    git_mounts: list[Any],
    project_id: uuid.UUID | None = None,
) -> None:
    """Validate git mount URLs.

    Simply checks that remote_url looks like a valid git URL.
    Actual clone validation happens at instance startup time.
    """
    for mount in git_mounts:
        remote_url = mount.get("remote_url")
        if not remote_url:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="Git mount missing remote_url",
            )

        if not remote_url.startswith(("http://", "https://", "git@", "ssh://")):
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail=f"Invalid git URL: {remote_url}",
            )


def _profile_to_response(
    profile: ConfigProfile, includes: list[ConfigProfileInclude] | None = None
) -> dict:
    return {
        "id": str(profile.id),
        "user_id": str(profile.user_id),
        "name": profile.name,
        "description": profile.description,
        "project_id": str(profile.project_id) if profile.project_id else None,
        "tool_type_id": str(profile.tool_type_id) if profile.tool_type_id else None,
        "env_vars": profile.env_vars or {},
        "runtime_hints": profile.runtime_hints or {},
        "mounts": profile.mounts or [],
        "git_mounts": profile.git_mounts or [],
        "files": profile.files or {},
        "is_default": profile.is_default,
        "includes": [
            {
                "id": str(inc.id),
                "included_profile_id": str(inc.included_profile_id),
                "order_index": inc.order_index,
            }
            for inc in (includes or profile.includes)
        ],
        "created_at": profile.created_at.isoformat() if profile.created_at else None,
        "updated_at": profile.updated_at.isoformat() if profile.updated_at else None,
    }


@router.get("", response_model=list[ConfigProfileResponse])
async def list_config_profiles(
    project_id: str | None = Query(None, description="Filter by project compatibility"),
    tool_type_id: str | None = Query(
        None, description="Filter by tool type compatibility"
    ),
    current_user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
):
    """List config profiles, optionally filtered by compatibility."""
    user_uuid = current_user_id
    query = (
        select(ConfigProfile)
        .where(ConfigProfile.user_id == user_uuid)
        .options(selectinload(ConfigProfile.includes))
    )

    if project_id or tool_type_id:
        # Compatibility filter: include portable profiles and matching scoped profiles
        project_uuid = uuid.UUID(project_id) if project_id else None
        tool_uuid = uuid.UUID(tool_type_id) if tool_type_id else None

        from sqlalchemy import or_

        conditions: list = []
        # Portable profiles (no project, no tool)
        conditions.append(
            (ConfigProfile.project_id.is_(None))
            & (ConfigProfile.tool_type_id.is_(None))
        )
        if project_uuid:
            # Profiles matching this project (with or without tool)
            conditions.append(ConfigProfile.project_id == project_uuid)
        if tool_uuid:
            # Profiles matching this tool (with or without project)
            conditions.append(ConfigProfile.tool_type_id == tool_uuid)
        if project_uuid and tool_uuid:
            # Exact match
            conditions.append(
                (ConfigProfile.project_id == project_uuid)
                & (ConfigProfile.tool_type_id == tool_uuid)
            )

        query = query.where(or_(*conditions))

    result = await session.execute(query)
    profiles = result.scalars().all()
    return [_profile_to_response(p) for p in profiles]


@router.post(
    "", response_model=ConfigProfileResponse, status_code=status.HTTP_201_CREATED
)
async def create_config_profile(
    data: ConfigProfileCreate,
    current_user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
):
    """Create a new config profile."""
    user_uuid = current_user_id

    # Check for duplicate name
    existing = await session.execute(
        select(ConfigProfile)
        .where(
            ConfigProfile.user_id == user_uuid,
            ConfigProfile.name == data.name,
        )
        .options(selectinload(ConfigProfile.includes))
    )
    if existing.scalar_one_or_none() is not None:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Profile with name '{data.name}' already exists",
        )

    # Validate references
    project_uuid = uuid.UUID(data.project_id) if data.project_id else None
    tool_uuid = uuid.UUID(data.tool_type_id) if data.tool_type_id else None
    await _check_access(session, user_uuid, project_uuid, tool_uuid)

    # Validate git mounts reference existing repositories
    if data.git_mounts:
        git_mounts_data = [
            m.model_dump() if hasattr(m, "model_dump") else m for m in data.git_mounts
        ]
        await _validate_git_mounts(session, user_uuid, git_mounts_data, project_uuid)

    # Check size
    size = _calculate_profile_size(data.model_dump())
    if size > MAX_PROFILE_SIZE_BYTES:
        raise HTTPException(
            status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
            detail=f"Profile size exceeds {MAX_PROFILE_SIZE_MB}MB limit",
        )

    profile = ConfigProfile(
        user_id=user_uuid,
        name=data.name,
        description=data.description,
        project_id=project_uuid,
        tool_type_id=tool_uuid,
        env_vars=data.env_vars,
        runtime_hints=data.runtime_hints,
        mounts=[m.model_dump() for m in data.mounts],
        git_mounts=[m.model_dump() for m in data.git_mounts],
        files=data.files,
        is_default=data.is_default,
    )
    session.add(profile)
    await session.commit()

    # Re-fetch with includes to avoid lazy loading issues
    result = await session.execute(
        select(ConfigProfile)
        .where(ConfigProfile.id == profile.id)
        .options(selectinload(ConfigProfile.includes))
    )
    profile = result.scalar_one()

    logger.debug("Created config profile %s for user %s", profile.id, user_uuid)
    return _profile_to_response(profile)


@router.get("/{profile_id}", response_model=ConfigProfileResponse)
async def get_config_profile(
    profile_id: str,
    current_user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
):
    """Get a config profile by ID."""
    profile = await _get_profile_with_includes(session, uuid.UUID(profile_id))
    if profile is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="Profile not found"
        )
    if profile.user_id != current_user_id:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized"
        )
    return _profile_to_response(profile)


@router.put("/{profile_id}", response_model=ConfigProfileResponse)
async def update_config_profile(
    profile_id: str,
    data: ConfigProfileUpdate,
    current_user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
):
    """Update a config profile."""
    profile = await _get_profile_with_includes(session, uuid.UUID(profile_id))
    if profile is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="Profile not found"
        )
    if profile.user_id != current_user_id:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized"
        )

    update_data = data.model_dump(exclude_unset=True)

    # Handle name uniqueness
    if "name" in update_data:
        existing = await session.execute(
            select(ConfigProfile).where(
                ConfigProfile.user_id == profile.user_id,
                ConfigProfile.name == update_data["name"],
                ConfigProfile.id != profile.id,
            )
        )
        if existing.scalar_one_or_none() is not None:
            raise HTTPException(
                status_code=status.HTTP_409_CONFLICT,
                detail=f"Profile with name '{update_data['name']}' already exists",
            )

    # Validate references
    project_uuid = (
        uuid.UUID(update_data["project_id"])
        if "project_id" in update_data and update_data["project_id"]
        else (profile.project_id if "project_id" not in update_data else None)
    )
    tool_uuid = (
        uuid.UUID(update_data["tool_type_id"])
        if "tool_type_id" in update_data and update_data["tool_type_id"]
        else (profile.tool_type_id if "tool_type_id" not in update_data else None)
    )
    await _check_access(session, profile.user_id, project_uuid, tool_uuid)

    # Validate git mounts reference existing repositories
    if "git_mounts" in update_data and update_data["git_mounts"] is not None:
        git_mounts_data = [
            m.model_dump() if hasattr(m, "model_dump") else m
            for m in update_data["git_mounts"]
        ]
        await _validate_git_mounts(
            session, profile.user_id, git_mounts_data, project_uuid
        )

    # Check size
    current_data = _profile_to_response(profile)
    merged = {**current_data, **update_data}
    size = _calculate_profile_size(merged)
    if size > MAX_PROFILE_SIZE_BYTES:
        raise HTTPException(
            status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
            detail=f"Profile size exceeds {MAX_PROFILE_SIZE_MB}MB limit",
        )

    # Apply updates
    for field_name, value in update_data.items():
        if field_name in ("project_id", "tool_type_id"):
            value = uuid.UUID(value) if value else None
        elif field_name == "mounts" and value is not None:
            value = [m.model_dump() if not isinstance(m, dict) else m for m in value]
        elif field_name == "git_mounts" and value is not None:
            value = [m.model_dump() if not isinstance(m, dict) else m for m in value]
        setattr(profile, field_name, value)

    await session.commit()

    # Re-fetch with includes to avoid lazy loading issues
    result = await session.execute(
        select(ConfigProfile)
        .where(ConfigProfile.id == profile.id)
        .options(selectinload(ConfigProfile.includes))
    )
    profile = result.scalar_one()

    logger.debug("Updated config profile %s", profile.id)
    return _profile_to_response(profile)


@router.delete("/{profile_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_config_profile(
    profile_id: str,
    current_user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
):
    """Delete a config profile."""
    profile = await _get_profile_with_includes(session, uuid.UUID(profile_id))
    if profile is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="Profile not found"
        )
    if profile.user_id != current_user_id:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized"
        )

    await session.delete(profile)
    await session.commit()

    logger.debug("Deleted config profile %s", profile_id)
    return None


@router.put("/{profile_id}/includes", response_model=ConfigProfileResponse)
async def update_profile_includes(
    profile_id: str,
    data: ConfigProfileIncludeUpdate,
    current_user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
):
    """Update the ordered includes for a config profile."""
    profile = await _get_profile_with_includes(session, uuid.UUID(profile_id))
    if profile is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="Profile not found"
        )
    if profile.user_id != current_user_id:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized"
        )

    # Validate all included profiles exist and belong to the user
    included_uuids = [uuid.UUID(inc_id) for inc_id in data.includes]
    for inc_uuid in included_uuids:
        inc_profile = await session.get(ConfigProfile, inc_uuid)
        if inc_profile is None:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail=f"Included profile not found: {inc_uuid}",
            )
        if inc_profile.user_id != current_user_id:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail=f"Not authorized to include profile: {inc_uuid}",
            )
        if inc_uuid == profile.id:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="Profile cannot include itself",
            )

    # Check for cycles
    cycle = await check_include_cycle(session, profile.id, None)
    if cycle is None and included_uuids:
        # Check each new include would not create a cycle
        for inc_uuid in included_uuids:
            cycle = await check_include_cycle(session, profile.id, inc_uuid)
            if cycle is not None:
                break

    if cycle is not None:
        cycle_str = " -> ".join(str(c) for c in cycle)
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Include cycle detected: {cycle_str}",
        )

    # Remove existing includes
    result = await session.execute(
        select(ConfigProfileInclude).where(
            ConfigProfileInclude.profile_id == profile.id
        )
    )
    for existing in result.scalars().all():
        await session.delete(existing)
    await session.flush()

    # Add new includes
    for order_index, inc_uuid in enumerate(included_uuids):
        include = ConfigProfileInclude(
            profile_id=profile.id,
            included_profile_id=inc_uuid,
            order_index=order_index,
        )
        session.add(include)
    await session.flush()

    await session.commit()

    # Re-fetch profile (includes loaded separately due to SQLite async issue)
    result = await session.execute(
        select(ConfigProfile).where(ConfigProfile.id == profile.id)
    )
    profile = result.scalar_one()

    inc_result = await session.execute(
        select(ConfigProfileInclude).where(
            ConfigProfileInclude.profile_id == profile.id
        )
    )
    direct_includes = inc_result.scalars().all()

    logger.debug("Updated includes for config profile %s", profile.id)
    return _profile_to_response(profile, list(direct_includes))


@router.get("/{profile_id}/preview")
async def preview_config_profile(
    profile_id: str,
    current_user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
):
    """Preview the resolved output of a config profile."""
    profile = await _get_profile_with_includes(session, uuid.UUID(profile_id))
    if profile is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="Profile not found"
        )
    if profile.user_id != current_user_id:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized"
        )

    try:
        resolved = await resolve_profile(session, profile.id)
    except ConfigProfileCycleError as exc:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=str(exc),
        )

    return resolved_profile_to_dict(resolved)


@router.get("/defaults/resolve")
async def resolve_default_profile(
    project_id: str = Query(..., description="Project ID"),
    tool_type_id: str = Query(..., description="Tool type ID"),
    current_user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
):
    """Resolve the default config profile for a project/tool combination.

    Selects by specificity:
    1. project+tool explicit default
    2. project explicit default
    3. tool explicit default
    4. global/user explicit default
    5. first created compatible profile
    6. none (returns null)
    """
    user_uuid = current_user_id
    project_uuid = uuid.UUID(project_id)
    tool_uuid = uuid.UUID(tool_type_id)

    # Fetch all compatible profiles ordered by created_at
    query = (
        select(ConfigProfile)
        .where(ConfigProfile.user_id == user_uuid)
        .where(
            (ConfigProfile.project_id.is_(None) & ConfigProfile.tool_type_id.is_(None))
            | (ConfigProfile.project_id == project_uuid)
            | (ConfigProfile.tool_type_id == tool_uuid)
            | (
                (ConfigProfile.project_id == project_uuid)
                & (ConfigProfile.tool_type_id == tool_uuid)
            )
        )
        .order_by(ConfigProfile.created_at)
    )
    result = await session.execute(query)
    profiles = result.scalars().all()

    if not profiles:
        return {"profile_id": None, "profile_name": None}

    # Check explicit defaults by specificity
    explicit_defaults = [p for p in profiles if p.is_default]

    # Most specific: project+tool
    for p in explicit_defaults:
        if p.project_id == project_uuid and p.tool_type_id == tool_uuid:
            return {"profile_id": str(p.id), "profile_name": p.name}

    # Next: project only
    for p in explicit_defaults:
        if p.project_id == project_uuid and p.tool_type_id is None:
            return {"profile_id": str(p.id), "profile_name": p.name}

    # Next: tool only
    for p in explicit_defaults:
        if p.project_id is None and p.tool_type_id == tool_uuid:
            return {"profile_id": str(p.id), "profile_name": p.name}

    # Next: global/user (no project, no tool)
    for p in explicit_defaults:
        if p.project_id is None and p.tool_type_id is None:
            return {"profile_id": str(p.id), "profile_name": p.name}

    # Fall back to first created compatible profile
    first = profiles[0]
    return {"profile_id": str(first.id), "profile_name": first.name}


class ValidateGitUrlRequest(BaseModel):
    url: str = Field(description="Git remote URL to validate")
    ssh_key_id: str | None = Field(
        default=None, description="Optional SSH key ID for private repos"
    )


class ValidateGitUrlResponse(BaseModel):
    valid: bool
    suggested_url: str | None = None
    branches: list[str] | None = None
    default_branch: str | None = None
    error: str | None = None
    error_code: str | None = None


@router.post("/validate-git-url", response_model=ValidateGitUrlResponse)
async def validate_git_url(
    data: ValidateGitUrlRequest,
    current_user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
) -> ValidateGitUrlResponse:
    """Validate a git remote URL and list available branches.

    Parses the URL, suggests corrections for browser URLs, and runs
    git ls-remote to verify reachability and enumerate branches.
    """
    parse_result = parse_git_url(data.url)
    original_url = data.url.strip()
    url_to_check = parse_result.get("base_url") or original_url

    if not url_to_check:
        return ValidateGitUrlResponse(
            valid=False,
            error=parse_result.get("message", "Invalid URL"),
            error_code=parse_result.get("error_code", "INVALID_URL"),
        )

    # If the URL needed parsing, return suggestion without checking remote
    if parse_result.get("needs_parsing") and url_to_check != original_url:
        return ValidateGitUrlResponse(
            valid=False,
            suggested_url=url_to_check,
            error=parse_result.get("message"),
            error_code=parse_result.get("error_code", "URL_NEEDS_PARSING"),
        )

    # Optional SSH key for private repos
    env = None
    key_path = None
    if data.ssh_key_id:
        from src.models.ssh_key import SSHKey
        from src.services.ssh_keys import _get_fernet

        try:
            ssh_key_uuid = uuid.UUID(data.ssh_key_id)
        except ValueError:
            return ValidateGitUrlResponse(
                valid=False,
                error="Invalid SSH key ID format",
                error_code="INVALID_SSH_KEY",
            )

        ssh_key = await session.get(SSHKey, ssh_key_uuid)
        if ssh_key is None or ssh_key.user_id != current_user_id:
            return ValidateGitUrlResponse(
                valid=False,
                error="SSH key not found or not authorized",
                error_code="SSH_KEY_NOT_FOUND",
            )

        import tempfile

        fernet = _get_fernet()
        private_key = fernet.decrypt(ssh_key.private_key_encrypted.encode()).decode()
        fd, key_path = tempfile.mkstemp(prefix="ssh_key_")
        try:
            os.write(fd, private_key.encode())
        finally:
            os.close(fd)
        os.chmod(key_path, 0o600)
        env = {
            "GIT_SSH_COMMAND": f"ssh -i {key_path} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
        }

    try:
        result = subprocess.run(
            ["git", "ls-remote", "--heads", url_to_check],
            capture_output=True,
            text=True,
            timeout=30,
            env={**os.environ, **env} if env else None,
        )
    except subprocess.TimeoutExpired:
        if key_path and os.path.exists(key_path):
            os.unlink(key_path)
        return ValidateGitUrlResponse(
            valid=False,
            error="Remote repository check timed out",
            error_code="TIMEOUT",
        )
    except FileNotFoundError:
        if key_path and os.path.exists(key_path):
            os.unlink(key_path)
        return ValidateGitUrlResponse(
            valid=False,
            error="git command not found on server",
            error_code="GIT_NOT_FOUND",
        )
    finally:
        if key_path and os.path.exists(key_path):
            os.unlink(key_path)

    if result.returncode != 0:
        stderr = result.stderr.strip()
        if (
            "could not resolve" in stderr.lower()
            or "unable to access" in stderr.lower()
        ):
            error_msg = "Could not reach repository. Check the URL and network access."
            error_code = "UNREACHABLE"
        elif (
            "authentication" in stderr.lower() or "permission denied" in stderr.lower()
        ):
            error_msg = (
                "Authentication failed. Provide an SSH key for private repositories."
            )
            error_code = "AUTH_FAILED"
        else:
            error_msg = f"Repository not accessible: {stderr[:200]}"
            error_code = "REMOTE_ERROR"
        return ValidateGitUrlResponse(
            valid=False,
            error=error_msg,
            error_code=error_code,
        )

    # Parse branches from ls-remote output
    branches: list[str] = []
    default_branch = "main"
    for line in result.stdout.strip().split("\n"):
        if not line.strip():
            continue
        parts = line.split()
        if len(parts) == 2:
            ref = parts[1]
            # refs/heads/branch-name
            if ref.startswith("refs/heads/"):
                branch_name = ref[len("refs/heads/") :]
                branches.append(branch_name)
                if branch_name in ("main", "master"):
                    default_branch = branch_name

    if not branches:
        return ValidateGitUrlResponse(
            valid=False,
            error="No branches found in remote repository",
            error_code="NO_BRANCHES",
        )

    return ValidateGitUrlResponse(
        valid=True,
        suggested_url=url_to_check if url_to_check != original_url else None,
        branches=branches,
        default_branch=default_branch,
    )
