import ast
import typing
from datetime import datetime
from http import HTTPStatus

import yaml
from fastapi import APIRouter, Depends, Query, Request
from fastapi.concurrency import run_in_threadpool
from sqlalchemy.orm import Session

import mlrun.api.crud
import mlrun.api.schemas
import mlrun.api.utils.clients.opa
import mlrun.errors
from mlrun.api.api import deps
from mlrun.api.api.utils import log_and_raise
from mlrun.config import config
from mlrun.k8s_utils import get_k8s_helper
from mlrun.utils import logger

router = APIRouter()


@router.get(
    "/projects/{project}/pipelines", response_model=mlrun.api.schemas.PipelinesOutput
)
def list_pipelines(
    project: str,
    namespace: str = config.namespace,
    sort_by: str = "",
    page_token: str = "",
    filter_: str = Query("", alias="filter"),
    format_: mlrun.api.schemas.PipelinesFormat = Query(
        mlrun.api.schemas.PipelinesFormat.metadata_only, alias="format"
    ),
    page_size: int = Query(None, gt=0, le=200),
    auth_verifier: mlrun.api.api.deps.AuthVerifierDep = Depends(
        mlrun.api.api.deps.AuthVerifierDep
    ),
    db_session: Session = Depends(deps.get_db_session),
):
    if project != "*":
        mlrun.api.utils.clients.opa.Client().query_project_permissions(
            project,
            mlrun.api.schemas.AuthorizationAction.read,
            auth_verifier.auth_info,
        )
    total_size, next_page_token, runs = None, None, []
    if get_k8s_helper(silent=True).is_running_inside_kubernetes_cluster():
        # we need to resolve the project from the returned run for the opa enforcement (project query param might be
        # "*"), so we can't really get back only the names here
        computed_format = (
            mlrun.api.schemas.PipelinesFormat.metadata_only
            if format_ == mlrun.api.schemas.PipelinesFormat.name_only
            else format_
        )
        total_size, next_page_token, runs = mlrun.api.crud.Pipelines().list_pipelines(
            db_session,
            project,
            namespace,
            sort_by,
            page_token,
            filter_,
            computed_format,
            page_size,
        )
    allowed_runs = mlrun.api.utils.clients.opa.Client().filter_project_resources_by_permissions(
        mlrun.api.schemas.AuthorizationResourceTypes.pipeline,
        runs,
        lambda run: (run["project"], run["id"],),
        auth_verifier.auth_info,
    )
    if format_ == mlrun.api.schemas.PipelinesFormat.name_only:
        allowed_runs = [run["name"] for run in allowed_runs]
    return mlrun.api.schemas.PipelinesOutput(
        runs=allowed_runs,
        total_size=total_size or 0,
        next_page_token=next_page_token or None,
    )


@router.post("/submit_pipeline")
@router.post("/submit_pipeline/")
# TODO: remove when 0.6.6 is no longer relevant
async def submit_pipeline_legacy(
    request: Request,
    namespace: str = config.namespace,
    experiment_name: str = Query("Default", alias="experiment"),
    run_name: str = Query("", alias="run"),
    auth_verifier: mlrun.api.api.deps.AuthVerifierDep = Depends(
        mlrun.api.api.deps.AuthVerifierDep
    ),
):
    response = await _create_pipeline(
        auth_verifier.auth_info, request, namespace, experiment_name, run_name,
    )
    return response


@router.post("/projects/{project}/pipelines")
async def create_pipeline(
    project: str,
    request: Request,
    namespace: str = config.namespace,
    experiment_name: str = Query("Default", alias="experiment"),
    run_name: str = Query("", alias="run"),
    auth_verifier: mlrun.api.api.deps.AuthVerifierDep = Depends(
        mlrun.api.api.deps.AuthVerifierDep
    ),
):
    response = await _create_pipeline(
        auth_verifier.auth_info, request, namespace, experiment_name, run_name, project
    )
    return response


async def _create_pipeline(
    auth_info: mlrun.api.schemas.AuthInfo,
    request: Request,
    namespace: str,
    experiment_name: str,
    run_name: str,
    project: typing.Optional[str] = None,
):
    # If we have the project (new clients from 0.7.0 uses the new endpoint in which it's mandatory) - check auth now
    if project:
        await run_in_threadpool(
            mlrun.api.utils.clients.opa.Client().query_project_resource_permissions,
            mlrun.api.schemas.AuthorizationResourceTypes.pipeline,
            project,
            "",
            mlrun.api.schemas.AuthorizationAction.create,
            auth_info,
        )
    run_name = run_name or experiment_name + " " + datetime.now().strftime(
        "%Y-%m-%d %H-%M-%S"
    )

    data = await request.body()
    if not data:
        log_and_raise(HTTPStatus.BAD_REQUEST.value, reason="Request body is empty")
    content_type = request.headers.get("content-type", "")

    # otherwise, best effort - try to parse it from the body - if successful - perform auth check - otherwise explode
    project = _try_resolve_project_from_body(content_type, data)
    if not project:
        raise mlrun.errors.MLRunInvalidArgumentError(
            "Pipelines can not be created without a project - you are probably running with old client - try upgrade to"
            " the server version"
        )
    else:
        await run_in_threadpool(
            mlrun.api.utils.clients.opa.Client().query_project_resource_permissions,
            mlrun.api.schemas.AuthorizationResourceTypes.pipeline,
            project,
            "",
            mlrun.api.schemas.AuthorizationAction.create,
            auth_info,
        )

    arguments = {}
    # TODO: stop reading "pipeline-arguments" header when 0.6.6 is no longer relevant
    arguments_data = request.headers.get("pipeline-arguments") or request.headers.get(
        mlrun.api.schemas.HeaderNames.pipeline_arguments
    )
    if arguments_data:
        arguments = ast.literal_eval(arguments_data)

    run = await run_in_threadpool(
        mlrun.api.crud.Pipelines().create_pipeline,
        experiment_name,
        run_name,
        content_type,
        data,
        arguments,
        namespace,
    )

    return {
        "id": run.id,
        "name": run.name,
    }


def _try_resolve_project_from_body(
    content_type: str, data: bytes
) -> typing.Optional[str]:
    if "/yaml" not in content_type:
        logger.warning(
            "Could not resolve project from body, unsupported content type",
            content_type=content_type,
        )
        return None
    workflow_manifest = yaml.load(data, Loader=yaml.FullLoader)
    return mlrun.api.crud.Pipelines().resolve_project_from_workflow_manifest(
        workflow_manifest
    )


@router.get("/pipelines/{run_id}")
@router.get("/pipelines/{run_id}/")
# TODO: remove when 0.6.6 is no longer relevant
def get_pipeline_legacy(
    run_id: str,
    namespace: str = Query(config.namespace),
    auth_verifier: mlrun.api.api.deps.AuthVerifierDep = Depends(
        mlrun.api.api.deps.AuthVerifierDep
    ),
    db_session: Session = Depends(deps.get_db_session),
):
    return _get_pipeline_without_project(
        db_session, auth_verifier.auth_info, run_id, namespace
    )


@router.get("/projects/{project}/pipelines/{run_id}")
def get_pipeline(
    run_id: str,
    project: str,
    namespace: str = Query(config.namespace),
    format_: mlrun.api.schemas.PipelinesFormat = Query(
        mlrun.api.schemas.PipelinesFormat.summary, alias="format"
    ),
    auth_verifier: mlrun.api.api.deps.AuthVerifierDep = Depends(
        mlrun.api.api.deps.AuthVerifierDep
    ),
    db_session: Session = Depends(deps.get_db_session),
):
    pipeline = mlrun.api.crud.Pipelines().get_pipeline(
        db_session, run_id, project, namespace, format_
    )
    mlrun.api.utils.clients.opa.Client().query_project_resource_permissions(
        mlrun.api.schemas.AuthorizationResourceTypes.pipeline,
        project,
        run_id,
        mlrun.api.schemas.AuthorizationAction.read,
        auth_verifier.auth_info,
    )
    return pipeline


def _get_pipeline_without_project(
    db_session: Session,
    auth_info: mlrun.api.schemas.AuthInfo,
    run_id: str,
    namespace: str,
):
    """
    This function is for when we receive a get pipeline request without the client specifying the project
    So we first get the pipeline, resolve the project out of it, and now that we know the project, we can verify
    permissions
    """
    run = mlrun.api.crud.Pipelines().get_pipeline(
        db_session,
        run_id,
        namespace=namespace,
        # minimal format that includes the project
        format_=mlrun.api.schemas.PipelinesFormat.summary,
    )
    mlrun.api.utils.clients.opa.Client().query_project_resource_permissions(
        mlrun.api.schemas.AuthorizationResourceTypes.pipeline,
        run["run"]["project"],
        run["run"]["id"],
        mlrun.api.schemas.AuthorizationAction.read,
        auth_info,
    )
    return run
