# Standard Library

# Third Party
from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi_cache.decorator import cache

# First Party
from resc_backend.constants import (
    CACHE_NAMESPACE_FINDING,
    CACHE_NAMESPACE_REPOSITORY,
    DEFAULT_RECORDS_PER_PAGE_LIMIT,
    ERROR_MESSAGE_500,
    ERROR_MESSAGE_503,
    REDIS_CACHE_EXPIRE,
    REPOSITORIES_TAG,
    RWS_ROUTE_DISTINCT_PROJECTS,
    RWS_ROUTE_DISTINCT_REPOSITORIES,
    RWS_ROUTE_FINDINGS_METADATA,
    RWS_ROUTE_LAST_SCAN,
    RWS_ROUTE_REPOSITORIES,
    RWS_ROUTE_SCANS,
)
from resc_backend.db.connection import Session
from resc_backend.resc_web_service.cache_manager import CacheManager
from resc_backend.resc_web_service.crud import repository as repository_crud
from resc_backend.resc_web_service.crud import scan as scan_crud
from resc_backend.resc_web_service.dependencies import get_db_connection
from resc_backend.resc_web_service.helpers.resc_swagger_models import Model404
from resc_backend.resc_web_service.schema import repository as repository_schema
from resc_backend.resc_web_service.schema import (
    repository_enriched as repository_enriched_schema,
)
from resc_backend.resc_web_service.schema import scan as scan_schema
from resc_backend.resc_web_service.schema.finding_count_model import FindingCountModel
from resc_backend.resc_web_service.schema.pagination_model import PaginationModel
from resc_backend.resc_web_service.schema.vcs_provider import VCSProviders

router = APIRouter(prefix=f"{RWS_ROUTE_REPOSITORIES}", tags=[REPOSITORIES_TAG])


@router.get(
    "",
    response_model=PaginationModel[repository_schema.RepositoryRead],
    summary="Get repositories",
    status_code=status.HTTP_200_OK,
    responses={
        200: {"description": "Retrieve all the repositories"},
        500: {"description": ERROR_MESSAGE_500},
        503: {"description": ERROR_MESSAGE_503},
    },
)
def get_all_repositories(
    skip: int = Query(default=0, ge=0),
    limit: int = Query(default=DEFAULT_RECORDS_PER_PAGE_LIMIT, ge=1),
    vcsproviders: list[VCSProviders] = Query(None, alias="vcsprovider", title="VCSProviders"),
    projectfilter: str | None = Query("", pattern=r"^[A-z0-9 .\-_%]*$"),
    repositoryfilter: str | None = Query("", pattern=r"^[A-z0-9 .\-_%]*$"),
    db_connection: Session = Depends(get_db_connection),
) -> PaginationModel[repository_schema.RepositoryRead]:
    """
        Retrieve all repository objects paginated

    - **db_connection**: Session of the database connection
    - **skip**: Integer amount of records to skip to support pagination
    - **limit**: Integer amount of records to return, to support pagination
    - **vcsproviders**: Optional, filter on supported vcs provider types
    - **projectfilter**: Optional, filter on project name. It is used as a string contains filter
    - **repositoryfilter**: Optional, filter on repository name. It is used as a string contains filter
    - **return**: [RepositoryRead]
        The output will contain a PaginationModel containing the list of RepositoryRead type objects,
        or an empty list if no repository
    """

    repositories = repository_crud.get_repositories(
        db_connection,
        skip=skip,
        limit=limit,
        vcs_providers=vcsproviders,
        project_filter=projectfilter,
        repository_filter=repositoryfilter,
    )

    total_repositories = repository_crud.get_repositories_count(
        db_connection,
        vcs_providers=vcsproviders,
        project_filter=projectfilter,
        repository_filter=repositoryfilter,
    )

    return PaginationModel[repository_schema.RepositoryRead](
        data=repositories, total=total_repositories, limit=limit, skip=skip
    )


@router.post(
    "",
    response_model=repository_schema.RepositoryRead,
    summary="Create a repository",
    status_code=status.HTTP_201_CREATED,
    responses={
        201: {"description": "Create a new repository"},
        500: {"description": ERROR_MESSAGE_500},
        503: {"description": ERROR_MESSAGE_503},
    },
)
async def create_repository(
    repository: repository_schema.RepositoryCreate,
    db_connection: Session = Depends(get_db_connection),
):
    """
        Create a repository with all the information

    - **db_connection**: Session of the database connection
    - **project_key**: each repository must have a project name or key
    - **repository_id**: repository id
    - **repository_name**: repository name
    - **repository_url**: repository url
    - **vcs_instance**: vcs instance id
    """
    repository = repository_crud.create_repository_if_not_exists(db_connection=db_connection, repository=repository)

    # Clear cache related to repository
    await CacheManager.clear_cache_by_namespace(namespace=CACHE_NAMESPACE_REPOSITORY)
    return repository


@router.get(
    "/{repository_id}",
    response_model=repository_schema.RepositoryRead,
    summary="Fetch a repository by ID",
    status_code=status.HTTP_200_OK,
    responses={
        200: {"description": "Retrieve repository <repository_id>"},
        404: {"model": Model404, "description": "Repository <repository_id> not found"},
        500: {"description": ERROR_MESSAGE_500},
        503: {"description": ERROR_MESSAGE_503},
    },
)
def read_repository(repository_id: int, db_connection: Session = Depends(get_db_connection)):
    """
        Read a repository by ID

    - **db_connection**: Session of the database connection
    - **repository_id**: ID of the repository for which details need to be fetched
    """
    db_repository = repository_crud.get_repository(db_connection, repository_id=repository_id)
    if db_repository is None:
        raise HTTPException(status_code=404, detail="Repository not found")
    return db_repository


@router.put(
    "/{repository_id}",
    response_model=repository_schema.RepositoryRead,
    summary="Update an existing repository",
    status_code=status.HTTP_200_OK,
    responses={
        200: {"description": "Update repository <repository_id>"},
        404: {"model": Model404, "description": "Repository <repository_id> not found"},
        500: {"description": ERROR_MESSAGE_500},
        503: {"description": ERROR_MESSAGE_503},
    },
)
async def update_repository(
    repository_id: int,
    repository: repository_schema.RepositoryCreate,
    db_connection: Session = Depends(get_db_connection),
):
    """
        Update an existing repository

    - **db_connection**: Session of the database connection
    - **repository_id**: ID of the repository
    - **project_key**: project name that needs to be updated
    - **repository_id**: repository id that needs to be updated
    - **repository_name**: repository name that needs to be updated
    - **repository_url**: repository url that needs to be updated
    - **vcs_instance**: vcs instance id that needs to be updated
    """
    db_repository = repository_crud.get_repository(db_connection, repository_id=repository_id)
    if db_repository is None:
        raise HTTPException(status_code=404, detail="Repository not found")

    updated_repository = repository_crud.update_repository(
        db_connection=db_connection, repository_id=repository_id, repository=repository
    )

    # Clear cache related to repository
    await CacheManager.clear_cache_by_namespace(namespace=CACHE_NAMESPACE_REPOSITORY)
    return updated_repository


@router.delete(
    "/{repository_id}",
    summary="Delete a repository",
    status_code=status.HTTP_200_OK,
    responses={
        200: {"description": "Delete repository <repository_id>"},
        404: {"model": Model404, "description": "Repository <repository_id> not found"},
        500: {"description": ERROR_MESSAGE_500},
        503: {"description": ERROR_MESSAGE_503},
    },
)
async def delete_repository(repository_id: int, db_connection: Session = Depends(get_db_connection)):
    """
        Delete a repository

    - **db_connection**: Session of the database connection
    - **repository_id**: ID of the repository to delete
    - **return**: The output will contain a success or error message based on the success of the deletion
    """
    db_repository = repository_crud.get_repository(db_connection, repository_id=repository_id)
    if db_repository is None:
        raise HTTPException(status_code=404, detail="Repository not found")
    repository_crud.delete_repository(db_connection, repository_id=repository_id, delete_related=True)

    # Clear cache related to repository
    await CacheManager.clear_cache_by_namespace(namespace=CACHE_NAMESPACE_REPOSITORY)
    return {"ok": True}


@router.get(
    f"{RWS_ROUTE_DISTINCT_PROJECTS}/",
    response_model=list[str],
    summary="Get all unique project names",
    status_code=status.HTTP_200_OK,
    responses={
        200: {"description": "Retrieve all the unique project-names"},
        500: {"description": ERROR_MESSAGE_500},
        503: {"description": ERROR_MESSAGE_503},
    },
)
@cache(namespace=CACHE_NAMESPACE_REPOSITORY, expire=REDIS_CACHE_EXPIRE)
def get_distinct_projects(
    vcsproviders: list[VCSProviders] = Query(None, alias="vcsprovider", title="VCSProviders"),
    repositoryfilter: str | None = Query("", pattern=r"^[A-z0-9 .\-_%]*$"),
    onlyifhasfindings: bool = Query(default=False),
    db_connection: Session = Depends(get_db_connection),
) -> list[str]:
    """
        Retrieve all unique project names

    - **db_connection**: Session of the database connection
    - **vcsproviders**: Optional, filter on supported vcs provider types
    - **repositoryfilter**: Optional, filter on repository name. It is used as a string contains filter
    - **onlyifhasfindings**: Optional, filter all projects those have findings
    - **return**: List[str]
        The output will contain a list of unique projects
    """

    distinct_projects = repository_crud.get_distinct_projects(
        db_connection,
        vcs_providers=vcsproviders,
        repository_filter=repositoryfilter,
        only_if_has_findings=onlyifhasfindings,
    )
    projects = [project.project_key for project in distinct_projects]
    return projects


@router.get(
    f"{RWS_ROUTE_DISTINCT_REPOSITORIES}/",
    response_model=list[str],
    summary="Get all unique repository names",
    status_code=status.HTTP_200_OK,
    responses={
        200: {"description": "Retrieve all the unique repository names"},
        500: {"description": ERROR_MESSAGE_500},
        503: {"description": ERROR_MESSAGE_503},
    },
)
@cache(namespace=CACHE_NAMESPACE_REPOSITORY, expire=REDIS_CACHE_EXPIRE)
def get_distinct_repositories(
    vcsproviders: list[VCSProviders] = Query(None, alias="vcsprovider", title="VCSProviders"),
    projectname: str | None = Query("", pattern=r"^[A-z0-9 .\-_%]*$"),
    onlyifhasfindings: bool = Query(default=False),
    db_connection: Session = Depends(get_db_connection),
) -> list[str]:
    """
        Retrieve all unique repository names

    - **db_connection**: Session of the database connection
    - **vcsproviders**: Optional, filter of supported vcs provider types
    - **projectname**: Optional, filter on project name. It is used as a full string match filter
    - **onlyifhasfindings**: Optional, filter all repositories that have findings
    - **return**: List[str]
        The output will contain a list of unique repositories
    """

    distinct_repositories = repository_crud.get_distinct_repositories(
        db_connection,
        vcs_providers=vcsproviders,
        project_name=projectname,
        only_if_has_findings=onlyifhasfindings,
    )
    repositories = [repo.repository_name for repo in distinct_repositories]
    return repositories


@router.get(
    "/{repository_id}" f"{RWS_ROUTE_FINDINGS_METADATA}",
    response_model=FindingCountModel[repository_schema.RepositoryRead],
    summary="Get findings metadata for a repository",
    status_code=status.HTTP_200_OK,
    responses={
        200: {"description": "Retrieve findings metadata for repository <repository_id>"},
        404: {"model": Model404, "description": "Repository <repository_id> not found"},
        500: {"description": ERROR_MESSAGE_500},
        503: {"description": ERROR_MESSAGE_503},
    },
)
def get_findings_metadata_for_repository(
    repository_id: int, db_connection: Session = Depends(get_db_connection)
) -> FindingCountModel[repository_schema.RepositoryRead]:
    """
        Retrieve findings metadata for a repository

    - **db_connection**: Session of the database connection
    - **repository_id**: ID of the repository object for which findings metadata to be retrieved
    - **return**: RepositoryRead, findings count per status
        The output will contain a RepositoryRead type object along with findings count per status,
        or empty if no scan was found
    """
    repository = repository_crud.get_repository(db_connection, repository_id=repository_id)
    if repository is None:
        raise HTTPException(status_code=404, detail="Repository not found")

    findings_meta_data = repository_crud.get_findings_metadata_by_repository_id(
        db_connection, repository_ids=[repository_id]
    )

    return FindingCountModel[repository_schema.RepositoryRead](
        data=repository,
        true_positive=findings_meta_data[repository_id]["true_positive"],
        false_positive=findings_meta_data[repository_id]["false_positive"],
        not_analyzed=findings_meta_data[repository_id]["not_analyzed"],
        not_accessible=findings_meta_data[repository_id]["not_accessible"],
        clarification_required=findings_meta_data[repository_id]["clarification_required"],
        outdated=findings_meta_data[repository_id]["outdated"],
        total_findings_count=findings_meta_data[repository_id]["total_findings_count"],
    )


@router.get(
    f"{RWS_ROUTE_FINDINGS_METADATA}/",
    response_model=PaginationModel[repository_enriched_schema.RepositoryEnrichedRead],
    summary="Get all repositories with findings metadata",
    status_code=status.HTTP_200_OK,
    responses={
        200: {"description": "Retrieve all the findings metadata for all the repositories"},
        500: {"description": ERROR_MESSAGE_500},
        503: {"description": ERROR_MESSAGE_503},
    },
)
@cache(namespace=CACHE_NAMESPACE_FINDING, expire=REDIS_CACHE_EXPIRE)
def get_all_repositories_with_findings_metadata(
    skip: int = Query(default=0, ge=0),
    limit: int = Query(default=DEFAULT_RECORDS_PER_PAGE_LIMIT, ge=1),
    vcsproviders: list[VCSProviders] = Query(None, alias="vcsprovider", title="VCSProviders"),
    projectfilter: str | None = Query("", pattern=r"^[A-z0-9 .\-_%]*$"),
    repositoryfilter: str | None = Query("", pattern=r"^[A-z0-9 .\-_%]*$"),
    onlyifhasfindings: bool = Query(default=False),
    db_connection: Session = Depends(get_db_connection),
) -> PaginationModel[repository_enriched_schema.RepositoryEnrichedRead]:
    """
        Retrieve all repository objects paginated

    - **db_connection**: Session of the database connection
    - **skip**: Integer amount of records to skip to support pagination
    - **limit**: Integer amount of records to return, to support pagination
    - **vcsproviders**: Optional, filter on supported vcs provider types
    - **projectfilter**: Optional, filter on project name. It is used as a string contains filter
    - **repositoryfilter**: Optional, filter on repository name. It is used as a string contains filter
    - **onlyifhasfindings**: Optional, filter all repositories those have findings
    - **return**: [RepositoryEnrichedRead]
        The output will contain a PaginationModel containing the list of RepositoryEnrichedRead type objects,
        or an empty list if no repository
    """

    repositories = repository_crud.get_repositories(
        db_connection,
        skip=skip,
        limit=limit,
        vcs_providers=vcsproviders,
        project_filter=projectfilter,
        repository_filter=repositoryfilter,
        only_if_has_findings=onlyifhasfindings,
    )

    total_repositories = repository_crud.get_repositories_count(
        db_connection,
        vcs_providers=vcsproviders,
        project_filter=projectfilter,
        repository_filter=repositoryfilter,
        only_if_has_findings=onlyifhasfindings,
    )
    repository_list = []
    repo_ids = [repo.id_ for repo in repositories]
    repo_findings_meta_data = repository_crud.get_findings_metadata_by_repository_id(
        db_connection, repository_ids=repo_ids
    )
    for repo in repositories:
        enriched_repository = repository_enriched_schema.RepositoryEnrichedRead(
            id_=repo.id_,
            project_key=repo.project_key,
            repository_id=repo.repository_id,
            repository_name=repo.repository_name,
            repository_url=repo.repository_url,
            vcs_provider=repo.provider_type,
            last_scan_id=repo.last_scan_id,
            last_scan_timestamp=repo.last_scan_timestamp,
            true_positive=repo_findings_meta_data[repo.id_]["true_positive"],
            false_positive=repo_findings_meta_data[repo.id_]["false_positive"],
            not_analyzed=repo_findings_meta_data[repo.id_]["not_analyzed"],
            not_accessible=repo_findings_meta_data[repo.id_]["not_accessible"],
            clarification_required=repo_findings_meta_data[repo.id_]["clarification_required"],
            outdated=repo_findings_meta_data[repo.id_]["outdated"],
            total_findings_count=repo_findings_meta_data[repo.id_]["total_findings_count"],
        )
        repository_list.append(enriched_repository)

    return PaginationModel[repository_enriched_schema.RepositoryEnrichedRead](
        data=repository_list, total=total_repositories, limit=limit, skip=skip
    )


@router.get(
    "/{repository_id}" f"{RWS_ROUTE_LAST_SCAN}",
    response_model=scan_schema.ScanRead,
    summary="Get latest scan for repository",
    status_code=status.HTTP_200_OK,
    responses={
        200: {"description": "Retrieve the latest scan related to a repository"},
        404: {"description": "Scan not found"},
        500: {"description": ERROR_MESSAGE_500},
        503: {"description": ERROR_MESSAGE_503},
    },
)
def get_last_scan_for_repository(
    repository_id: int, db_connection: Session = Depends(get_db_connection)
) -> scan_schema.ScanRead:
    """
        Retrieve the latest scan object related to a repository

    - **db_connection**: Session of the database connection
    - **repository_id**: ID of the parent repository object for which scan objects to be retrieved
    - **return**: ScanRead
        The output will contain a ScanRead type object,
        or empty if no scan was found
    """
    last_scan = scan_crud.get_latest_scan_for_repository(db_connection, repository_id=repository_id)
    if last_scan is None:
        raise HTTPException(status_code=404, detail="Scan not found")
    return last_scan


@router.get(
    "/{repository_id}" f"{RWS_ROUTE_SCANS}",
    summary="Get scans for repository",
    response_model=PaginationModel[scan_schema.ScanRead],
    status_code=status.HTTP_200_OK,
    responses={
        200: {"description": "Retrieve all the scans related to a repository"},
        500: {"description": ERROR_MESSAGE_500},
        503: {"description": ERROR_MESSAGE_503},
    },
)
def get_scans_for_repository(
    repository_id: int,
    skip: int = Query(default=0, ge=0),
    limit: int = Query(default=DEFAULT_RECORDS_PER_PAGE_LIMIT, ge=1),
    db_connection: Session = Depends(get_db_connection),
) -> PaginationModel[scan_schema.ScanRead]:
    """
        Retrieve all scan objects related to a repository paginated

    - **db_connection**: Session of the database connection
    - **repository_id**: ID of the parent repository object for which scan objects to be retrieved
    - **skip**: Integer amount of records to skip to support pagination
    - **limit**: Integer amount of records to return, to support pagination
    - **return**: [ScanRead]
        The output will contain a PaginationModel containing the list of ScanRead type objects,
        or an empty list if no scan was found
    """
    scans = scan_crud.get_scans(db_connection, skip=skip, limit=limit, repository_id=repository_id)

    total_scans = scan_crud.get_scans_count(db_connection, repository_id=repository_id)

    return PaginationModel[scan_schema.ScanRead](data=scans, total=total_scans, limit=limit, skip=skip)
