import logging
import os
import shutil
import tempfile
from typing import (
    cast,
    IO,
    Optional,
    Union,
)

from fastapi import (
    Body,
    Depends,
    Request,
    Response,
    status,
    UploadFile,
)
from starlette.datastructures import UploadFile as StarletteUploadFile

from galaxy.exceptions import (
    ActionInputError,
    InsufficientPermissionsException,
    RequestParameterInvalidException,
)
from galaxy.webapps.galaxy.api import as_form
from tool_shed.context import SessionRequestContext
from tool_shed.managers.repositories import (
    add_admin_user,
    can_manage_repo,
    can_update_repo,
    check_updates,
    create_repository,
    ensure_can_manage,
    get_install_info,
    get_ordered_installable_revisions,
    get_repository_metadata_dict,
    get_repository_metadata_for_management,
    index_repositories,
    index_repositories_paginated,
    IndexRequest,
    PaginatedIndexRequest,
    readmes,
    remove_admin_user,
    reset_metadata_on_repositories,
    reset_metadata_on_repository,
    search,
    to_detailed_model,
    to_model,
    UpdatesRequest,
    upload_tar_and_set_metadata,
    usernames_with_admin_role,
)
from tool_shed.structured_app import ToolShedApp
from tool_shed.util.repository_util import (
    get_repository_in_tool_shed,
    update_validated_repository,
)
from tool_shed_client.schema import (
    CreateRepositoryRequest,
    DetailedRepository,
    from_legacy_install_info,
    IndexSortByType,
    InstallInfo,
    PaginatedRepositoryIndexResults,
    Repository,
    RepositoryMetadata,
    RepositoryPermissions,
    RepositoryRevisionReadmes,
    RepositorySearchResults,
    RepositoryUpdate,
    RepositoryUpdateRequest,
    ResetMetadataOnRepositoriesRequest,
    ResetMetadataOnRepositoriesResponse,
    ResetMetadataOnRepositoryRequest,
    ResetMetadataOnRepositoryResponse,
    UpdateRepositoryRequest,
    ValidRepostiroyUpdateMessage,
)
from . import (
    ChangesetRevisionPathParam,
    CommitMessageQueryParam,
    depend_on_either_json_or_form_data,
    depends,
    DependsOnTrans,
    DownloadableQueryParam,
    DryRunQueryParam,
    OptionalHexlifyParam,
    OptionalRepositoryIdParam,
    OptionalRepositoryNameParam,
    OptionalRepositoryOwnerParam,
    RepositoryIdPathParam,
    RepositoryIndexCategoryQueryParam,
    RepositoryIndexDeletedQueryParam,
    RepositoryIndexFilterParam,
    RepositoryIndexNameQueryParam,
    RepositoryIndexOwnerQueryParam,
    RepositoryIndexQueryParam,
    RepositoryIndexSortByParam,
    RepositoryIndexSortDescParam,
    RepositorySearchPageQueryParam,
    RepositorySearchPageSizeQueryParam,
    RequiredChangesetParam,
    RequiredRepoNameParam,
    RequiredRepoOwnerParam,
    RequiredRepositoryChangesetRevisionParam,
    Router,
    UsernameIdPathParam,
    VerboseQueryParam,
)

log = logging.getLogger(__name__)


router = Router(tags=["repositories"])

IndexResponse = Union[RepositorySearchResults, list[Repository], PaginatedRepositoryIndexResults]


@as_form
class RepositoryUpdateRequestFormData(RepositoryUpdateRequest):
    pass


@router.cbv
class FastAPIRepositories:
    app: ToolShedApp = depends(ToolShedApp)

    @router.get(
        "/api/repositories",
        description="Get a list of repositories or perform a search.",
        operation_id="repositories__index",
    )
    def index(
        self,
        q: Optional[str] = RepositoryIndexQueryParam,
        filter: Optional[str] = RepositoryIndexFilterParam,
        page: Optional[int] = RepositorySearchPageQueryParam,
        page_size: Optional[int] = RepositorySearchPageSizeQueryParam,
        deleted: Optional[bool] = RepositoryIndexDeletedQueryParam,
        owner: Optional[str] = RepositoryIndexOwnerQueryParam,
        name: Optional[str] = RepositoryIndexNameQueryParam,
        category_id: Optional[str] = RepositoryIndexCategoryQueryParam,
        sort_desc: Optional[bool] = RepositoryIndexSortDescParam,
        sort_by: Optional[IndexSortByType] = RepositoryIndexSortByParam,
        trans: SessionRequestContext = DependsOnTrans,
    ) -> IndexResponse:

        if q and filter:
            raise RequestParameterInvalidException(
                "Cannot specify both the 'q' and 'filter' parameter at the same time."
            )

        if q:
            page = page or 1
            assert page_size is not None
            search_results = search(trans, q, page, page_size)
            return RepositorySearchResults(**search_results)
        # See API notes - was added in https://github.com/galaxyproject/galaxy/pull/3626/files
        # but I think is currently unused. So probably we should just drop it until someone
        # complains.
        # elif params.tool_ids:
        #    response = index_tool_ids(self.app, params.tool_ids)
        #    return response
        elif page:
            assert page_size is not None
            paginated_index_request = PaginatedIndexRequest(
                page=page,
                page_size=page_size,
                owner=owner,
                name=name,
                deleted=deleted or False,
                filter=filter,
                category_id=category_id,
                sort_by=sort_by,
                sort_desc=sort_desc,
            )
            paginated_repositories = index_repositories_paginated(self.app, paginated_index_request)
            return paginated_repositories
        else:
            index_request = IndexRequest(
                owner=owner,
                name=name,
                deleted=deleted or False,
                filter=filter,
                category_id=category_id,
                sort_by=sort_by,
                sort_desc=sort_desc,
            )
            repositories = index_repositories(self.app, index_request)
            return [to_model(self.app, r) for r in repositories]

    @router.get(
        "/api/repositories/get_repository_revision_install_info",
        description="Get information used by the install client to install this repository.",
        operation_id="repositories__legacy_install_info",
    )
    def legacy_install_info(
        self,
        trans: SessionRequestContext = DependsOnTrans,
        name: str = RequiredRepoNameParam,
        owner: str = RequiredRepoOwnerParam,
        changeset_revision: str = RequiredChangesetParam,
    ) -> list:
        legacy_install_info = get_install_info(
            trans,
            name,
            owner,
            changeset_revision,
        )
        return list(legacy_install_info)

    @router.get(
        "/api/repositories/install_info",
        description="Get information used by the install client to install this repository.",
        operation_id="repositories__install_info",
    )
    def install_info(
        self,
        trans: SessionRequestContext = DependsOnTrans,
        name: str = RequiredRepoNameParam,
        owner: str = RequiredRepoOwnerParam,
        changeset_revision: str = RequiredChangesetParam,
    ) -> InstallInfo:
        # A less problematic version of the above API, but I guess we
        # need to maintain the older version for older Galaxy API clients
        # for... sometime... or forever.
        legacy_install_info = get_install_info(
            trans,
            name,
            owner,
            changeset_revision,
        )
        return from_legacy_install_info(legacy_install_info)

    @router.get(
        "/api/repositories/{encoded_repository_id}/metadata",
        description="Get information about repository metadata",
        operation_id="repositories__metadata",
        # See comment below.
        # response_model=RepositoryMetadata,
    )
    def metadata(
        self,
        encoded_repository_id: str = RepositoryIdPathParam,
        downloadable_only: bool = DownloadableQueryParam,
    ) -> dict:
        recursive = True
        as_dict = get_repository_metadata_dict(self.app, encoded_repository_id, recursive, downloadable_only)
        # fails 1020 if we try to use the model - I guess repository dependencies
        # are getting lost
        return as_dict

    @router.get(
        "/api_internal/repositories/{encoded_repository_id}/metadata",
        description="Get information about repository metadata",
        operation_id="repositories__internal_metadata",
        response_model=RepositoryMetadata,
    )
    def metadata_internal(
        self,
        encoded_repository_id: str = RepositoryIdPathParam,
        downloadable_only: bool = DownloadableQueryParam,
    ) -> RepositoryMetadata:
        recursive = True
        as_dict = get_repository_metadata_dict(self.app, encoded_repository_id, recursive, downloadable_only)
        return RepositoryMetadata(root=as_dict)

    @router.get(
        "/api/repositories/get_ordered_installable_revisions",
        description="Get an ordered list of the repository changeset revisions that are installable",
        operation_id="repositories__get_ordered_installable_revisions",
    )
    def get_ordered_installable_revisions(
        self,
        owner: Optional[str] = OptionalRepositoryOwnerParam,
        name: Optional[str] = OptionalRepositoryNameParam,
        tsr_id: Optional[str] = OptionalRepositoryIdParam,
    ) -> list[str]:
        return get_ordered_installable_revisions(self.app, name, owner, tsr_id)

    @router.post(
        "/api/repositories/reset_metadata_on_repository",
        description="reset metadata on a repository",
        operation_id="repositories__reset_legacy",
    )
    def reset_metadata_on_repository_legacy(
        self,
        trans: SessionRequestContext = DependsOnTrans,
        request: ResetMetadataOnRepositoryRequest = depend_on_either_json_or_form_data(
            ResetMetadataOnRepositoryRequest
        ),
    ) -> ResetMetadataOnRepositoryResponse:
        return reset_metadata_on_repository(
            trans, request.repository_id, dry_run=request.dry_run, verbose=request.verbose
        )

    @router.post(
        "/api/repositories/{encoded_repository_id}/reset_metadata",
        description="reset metadata on a repository",
        operation_id="repositories__reset",
    )
    def reset_metadata_on_repository(
        self,
        trans: SessionRequestContext = DependsOnTrans,
        encoded_repository_id: str = RepositoryIdPathParam,
        dry_run: bool = DryRunQueryParam,
        verbose: bool = VerboseQueryParam,
    ) -> ResetMetadataOnRepositoryResponse:
        return reset_metadata_on_repository(trans, encoded_repository_id, dry_run=dry_run, verbose=verbose)

    @router.post(
        "/api/repositories/reset_metadata_on_repositories",
        description="reset metadata on all of your repositories",
        operation_id="repositories__reset_all",
    )
    def reset_metadata_on_repositories(
        self,
        trans: SessionRequestContext = DependsOnTrans,
        request: ResetMetadataOnRepositoriesRequest = depend_on_either_json_or_form_data(
            ResetMetadataOnRepositoriesRequest
        ),
    ) -> ResetMetadataOnRepositoriesResponse:
        return reset_metadata_on_repositories(trans, request)

    @router.get(
        "/api/repositories/updates",
        operation_id="repositories__update",
    )
    @router.get(
        "/api/repositories/updates/",
    )
    def updates(
        self,
        owner: Optional[str] = OptionalRepositoryOwnerParam,
        name: Optional[str] = OptionalRepositoryNameParam,
        changeset_revision: str = RequiredRepositoryChangesetRevisionParam,
        hexlify: Optional[bool] = OptionalHexlifyParam,
    ):
        request = UpdatesRequest(
            name=name,
            owner=owner,
            changeset_revision=changeset_revision,
            hexlify=hexlify,
        )
        return Response(content=check_updates(self.app, request))

    @router.post(
        "/api/repositories",
        description="create a new repository",
        operation_id="repositories__create",
    )
    def create(
        self,
        trans: SessionRequestContext = DependsOnTrans,
        request: CreateRepositoryRequest = Body(...),
    ) -> Repository:
        db_repository = create_repository(
            trans,
            request,
        )
        return to_model(self.app, db_repository)

    @router.get(
        "/api/repositories/{encoded_repository_id}",
        operation_id="repositories__show",
    )
    def show(
        self,
        encoded_repository_id: str = RepositoryIdPathParam,
    ) -> DetailedRepository:
        repository = get_repository_in_tool_shed(self.app, encoded_repository_id)
        return to_detailed_model(self.app, repository)

    @router.put(
        "/api/repositories/{encoded_repository_id}",
        operation_id="repositories__update_repository",
    )
    def update_repository(
        self,
        trans: SessionRequestContext = DependsOnTrans,
        encoded_repository_id: str = RepositoryIdPathParam,
        request: UpdateRepositoryRequest = Body(...),
    ) -> DetailedRepository:
        repository = get_repository_in_tool_shed(self.app, encoded_repository_id)
        ensure_can_manage(trans, repository)

        # may want to set some of these to null, so we're using the exclude_unset feature
        # to just serialize the ones we want to use to a dictionary.
        # by_alias=True ensures type_ → type, matching update_validated_repository's expected keys.
        update_dictionary = request.model_dump(exclude_unset=True, by_alias=True)
        repo_result, message = update_validated_repository(trans, repository, **update_dictionary)
        if repo_result is None:
            raise ActionInputError(message)

        return to_detailed_model(self.app, repository)

    @router.get(
        "/api/repositories/{encoded_repository_id}/permissions",
        operation_id="repositories__permissions",
    )
    def permissions(
        self,
        trans: SessionRequestContext = DependsOnTrans,
        encoded_repository_id: str = RepositoryIdPathParam,
    ) -> RepositoryPermissions:
        repository = get_repository_in_tool_shed(self.app, encoded_repository_id)
        if not can_update_repo(trans, repository):
            raise InsufficientPermissionsException(
                "You do not have permission to inspect repository repository permissions."
            )
        return RepositoryPermissions(
            allow_push=trans.app.security_agent.usernames_that_can_push(repository),
            can_manage=can_manage_repo(trans, repository),
            can_push=can_update_repo(trans, repository),
        )

    @router.get(
        "/api/repositories/{encoded_repository_id}/allow_push",
        operation_id="repositories__show_allow_push",
    )
    def show_allow_push(
        self,
        trans: SessionRequestContext = DependsOnTrans,
        encoded_repository_id: str = RepositoryIdPathParam,
    ) -> list[str]:
        repository = get_repository_in_tool_shed(self.app, encoded_repository_id)
        ensure_can_manage(trans, repository)
        return trans.app.security_agent.usernames_that_can_push(repository)

    @router.post(
        "/api/repositories/{encoded_repository_id}/allow_push/{username}",
        operation_id="repositories__add_allow_push",
    )
    def add_allow_push(
        self,
        trans: SessionRequestContext = DependsOnTrans,
        encoded_repository_id: str = RepositoryIdPathParam,
        username: str = UsernameIdPathParam,
    ) -> list[str]:
        repository = get_repository_in_tool_shed(self.app, encoded_repository_id)
        if not can_manage_repo(trans, repository):
            raise InsufficientPermissionsException("You do not have permission to update this repository.")
        repository.set_allow_push([username])
        return trans.app.security_agent.usernames_that_can_push(repository)

    @router.put(
        "/api/repositories/{encoded_repository_id}/revisions/{changeset_revision}/malicious",
        operation_id="repositories__set_malicious",
        status_code=status.HTTP_204_NO_CONTENT,
    )
    def set_malicious(
        self,
        trans: SessionRequestContext = DependsOnTrans,
        encoded_repository_id: str = RepositoryIdPathParam,
        changeset_revision: str = ChangesetRevisionPathParam,
    ):
        repository_metadata = get_repository_metadata_for_management(trans, encoded_repository_id, changeset_revision)
        repository_metadata.malicious = True
        trans.sa_session.add(repository_metadata)
        trans.sa_session.commit()
        return Response(status_code=status.HTTP_204_NO_CONTENT)

    @router.delete(
        "/api/repositories/{encoded_repository_id}/revisions/{changeset_revision}/malicious",
        operation_id="repositories__unset_malicious",
        status_code=status.HTTP_204_NO_CONTENT,
    )
    def unset_malicious(
        self,
        trans: SessionRequestContext = DependsOnTrans,
        encoded_repository_id: str = RepositoryIdPathParam,
        changeset_revision: str = ChangesetRevisionPathParam,
    ):
        repository_metadata = get_repository_metadata_for_management(trans, encoded_repository_id, changeset_revision)
        repository_metadata.malicious = False
        trans.sa_session.add(repository_metadata)
        trans.sa_session.commit()
        return Response(status_code=status.HTTP_204_NO_CONTENT)

    @router.put(
        "/api/repositories/{encoded_repository_id}/deprecated",
        operation_id="repositories__set_deprecated",
        status_code=status.HTTP_204_NO_CONTENT,
    )
    def set_deprecated(
        self,
        trans: SessionRequestContext = DependsOnTrans,
        encoded_repository_id: str = RepositoryIdPathParam,
    ):
        repository = get_repository_in_tool_shed(self.app, encoded_repository_id)
        ensure_can_manage(trans, repository)
        repository.deprecated = True
        trans.sa_session.add(repository)
        trans.sa_session.commit()
        return Response(status_code=status.HTTP_204_NO_CONTENT)

    @router.delete(
        "/api/repositories/{encoded_repository_id}/deprecated",
        operation_id="repositories__unset_deprecated",
        status_code=status.HTTP_204_NO_CONTENT,
    )
    def unset_deprecated(
        self,
        trans: SessionRequestContext = DependsOnTrans,
        encoded_repository_id: str = RepositoryIdPathParam,
    ):
        repository = get_repository_in_tool_shed(self.app, encoded_repository_id)
        ensure_can_manage(trans, repository)
        repository.deprecated = False
        trans.sa_session.add(repository)
        trans.sa_session.commit()
        return Response(status_code=status.HTTP_204_NO_CONTENT)

    @router.delete(
        "/api/repositories/{encoded_repository_id}/allow_push/{username}",
        operation_id="repositories__remove_allow_push",
    )
    def remove_allow_push(
        self,
        trans: SessionRequestContext = DependsOnTrans,
        encoded_repository_id: str = RepositoryIdPathParam,
        username: str = UsernameIdPathParam,
    ) -> list[str]:
        repository = get_repository_in_tool_shed(self.app, encoded_repository_id)
        if not can_manage_repo(trans, repository):
            raise InsufficientPermissionsException("You do not have permission to update this repository.")
        repository.set_allow_push(None, remove_auth=username)
        return trans.app.security_agent.usernames_that_can_push(repository)

    @router.get(
        "/api/repositories/{encoded_repository_id}/admins",
        operation_id="repositories__show_admins",
    )
    def show_admins(
        self,
        trans: SessionRequestContext = DependsOnTrans,
        encoded_repository_id: str = RepositoryIdPathParam,
    ) -> list[str]:
        repository = get_repository_in_tool_shed(self.app, encoded_repository_id)
        ensure_can_manage(trans, repository)
        return usernames_with_admin_role(self.app, repository)

    @router.post(
        "/api/repositories/{encoded_repository_id}/admins/{username}",
        operation_id="repositories__add_admin",
    )
    def add_admin(
        self,
        trans: SessionRequestContext = DependsOnTrans,
        encoded_repository_id: str = RepositoryIdPathParam,
        username: str = UsernameIdPathParam,
    ) -> list[str]:
        repository = get_repository_in_tool_shed(self.app, encoded_repository_id)
        ensure_can_manage(trans, repository)
        return add_admin_user(self.app, repository, username)

    @router.delete(
        "/api/repositories/{encoded_repository_id}/admins/{username}",
        operation_id="repositories__remove_admin",
    )
    def remove_admin(
        self,
        trans: SessionRequestContext = DependsOnTrans,
        encoded_repository_id: str = RepositoryIdPathParam,
        username: str = UsernameIdPathParam,
    ) -> list[str]:
        repository = get_repository_in_tool_shed(self.app, encoded_repository_id)
        ensure_can_manage(trans, repository)
        return remove_admin_user(self.app, repository, username)

    @router.post(
        "/api/repositories/{encoded_repository_id}/changeset_revision",
        description="upload new revision to the repository",
        operation_id="repositories__create_revision",
    )
    async def create_changeset_revision(
        self,
        request: Request,
        encoded_repository_id: str = RepositoryIdPathParam,
        commit_message: Optional[str] = CommitMessageQueryParam,
        trans: SessionRequestContext = DependsOnTrans,
        files: Optional[list[UploadFile]] = None,
        revision_request: RepositoryUpdateRequest = Depends(RepositoryUpdateRequestFormData.as_form),  # type: ignore[attr-defined]
    ) -> RepositoryUpdate:
        try:
            # Code stolen from Marius' work in Galaxy's Tools API.

            files2: list[StarletteUploadFile] = cast(list[StarletteUploadFile], files or [])
            # FastAPI's UploadFile is a very light wrapper around starlette's UploadFile
            if not files2:
                data = await request.form()
                for value in data.values():
                    if isinstance(value, StarletteUploadFile):
                        files2.append(value)

            repository = get_repository_in_tool_shed(self.app, encoded_repository_id)

            if not can_update_repo(trans, repository):
                raise InsufficientPermissionsException("You do not have permission to update this repository.")

            assert trans.user
            assert files2
            the_file = files2[0]
            with tempfile.NamedTemporaryFile(
                dir=trans.app.config.new_file_path, prefix="upload_file_data_", delete=False
            ) as dest:
                upload_file_like: IO[bytes] = the_file.file
                shutil.copyfileobj(upload_file_like, dest)
            the_file.file.close()
            filename = dest.name
            try:
                message = upload_tar_and_set_metadata(
                    trans,
                    trans.request.host,
                    repository,
                    filename,
                    commit_message or revision_request.commit_message or "Uploaded",
                )
                return RepositoryUpdate(root=ValidRepostiroyUpdateMessage(message=message))
            finally:
                if os.path.exists(filename):
                    os.remove(filename)
        except Exception:
            log.exception("Problem in here...")
            raise

    @router.get(
        "/api/repositories/{encoded_repository_id}/revisions/{changeset_revision}/readmes",
        description="fetch readmes for repository revision",
        operation_id="repositories__readmes",
        response_model=RepositoryRevisionReadmes,
    )
    def get_readmes(
        self,
        encoded_repository_id: str = RepositoryIdPathParam,
        changeset_revision: str = ChangesetRevisionPathParam,
    ) -> dict:
        repository = get_repository_in_tool_shed(self.app, encoded_repository_id)
        return readmes(self.app, repository, changeset_revision)
