#!/usr/bin/env python
from __future__ import annotations

# Standard imports
import json
from datetime import datetime
from io import TextIOWrapper
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import List, Union, Dict, Any, Optional
import re

# Libica apis
from libica.openapi.v2.api.project_analysis_api import ProjectAnalysisApi
from libica.openapi.v2.api_client import ApiClient
from libica.openapi.v2.exceptions import ApiException

# Wrapica imports
from ...enums import ProjectAnalysisStatus, ProjectAnalysisSortParameters, AnalysisLogStreamName, \
    ProjectAnalysisStepStatus

# Libica models
from libica.openapi.v2.models import (
    AnalysisQueryParameters,
    AnalysisV3,
    AnalysisV4,
    AnalysisInput,
    AnalysisOutput,
    AnalysisOutputList,
    AnalysisStep,
    AnalysisStepLogs,
    CwlAnalysisInputJson,
    CwlAnalysisOutputJson,
)

# Local imports
from ...utils.globals import LIBICAV2_DEFAULT_PAGE_SIZE, IS_REGEX_MATCH
from ...utils.configuration import get_icav2_configuration
from ...utils.miscell import is_uuid_format
from ...utils.websocket_helpers import write_websocket_to_file, convert_html_to_text
from ...utils.logger import get_logger

AnalysisType = Union[AnalysisV3, AnalysisV4]

logger = get_logger()


def get_project_analysis_inputs(
    project_id: str,
    analysis_id: str
) -> List[AnalysisInput]:
    """
    Get the analysis inputs for a given analysis

    :param project_id: The project context the analysis was run in
    :param analysis_id: The analysis id to query

    :return: List of analysis inputs
    :rtype: List[`AnalysisInput <https://umccr-illumina.github.io/libica/openapi/v2/docs/AnalysisInput>`_]

    :raises: ApiException

    :Examples:

    .. code-block:: python

        :linenos:
        from wrapica.project_analysis import get_project_analysis_inputs

        # Set params
        project_id = "project_id"
        analysis_id = "analysis_id"

        workflow_inputs = get_project_analysis_inputs(project_id, analysis_id)

    """
    # Enter a context with an instance of the API client
    with ApiClient(get_icav2_configuration()) as api_client:
        # Create an instance of the API class
        api_instance = ProjectAnalysisApi(api_client)

        # example passing only required values which don't have defaults set
        try:
            # Retrieve the outputs of an analysis.
            analysis_input_list: List[AnalysisInput] = api_instance.get_analysis_inputs(
                project_id, analysis_id
            ).items
        except ApiException as e:
            logger.error("Exception when calling ProjectAnalysisApi->get_analysis_outputs: %s\n" % e)
            raise ApiException

    return analysis_input_list


def get_analysis_input_object_from_analysis_input_code(
    project_id: str,
    analysis_id: str,
    analysis_input_code: str
) -> AnalysisInput:
    """
    Given an analysis input code for an analysis id, collect the analysis input object
    Confirm the input has either analysis or external data attributes

    :param project_id: The project context the analysis was run in
    :param analysis_id: The analysis id to query
    :param analysis_input_code: The analysis input code to query

    :return: The analysis input object
    :rtype: `AnalysisInput <https://umccr-illumina.github.io/libica/openapi/v2/docs/AnalysisInput>`_

    :raises: StopIteration, ValueError, ApiException

    :Examples:

    .. code-block:: python

        :linenos:
        from wrapica.project_analysis import get_analysis_input_object_from_analysis_code

        # Set params
        project_id = "project_id"
        analysis_id = "analysis_id"
        analysis_code = "run_folder"

        run_folder_input_data_id = get_analysis_input_object_from_analysis_code(
            project_id, analysis_id, analysis_code
        ).analysis_data[0].data_id
    """
    # Get analysis inputs
    analysis_input_list: List[AnalysisInput] = get_project_analysis_inputs(
        project_id,
        analysis_id
    )

    # Iterate through inputs to find the one we want
    try:
        input_obj: AnalysisInput = next(
            filter(
                lambda analysis_input_iter: analysis_input_iter.code == analysis_input_code,
                analysis_input_list
            )
        )
    except StopIteration:
        logger.error(f"Could not get {analysis_input_code} from analysis {analysis_id}")
        raise StopIteration

    if len(input_obj.analysis_data) == 0 and len(input_obj.external_data) == 0:
        logger.error(f"Expected analysis data or external data to be 1 but got {len(input_obj.analysis_data)}")
        raise ValueError

    return input_obj


def get_outputs_object_from_analysis_id(
    project_id: str,
    analysis_id: str
) -> List[AnalysisOutput]:
    """
    Query the outputs object from the analysis id

    :param project_id: The project context the analysis was run in
    :param analysis_id: The analysis id to query

    :return: List of analysis outputs
    :rtype: List[`AnalysisOutput <https://umccr-illumina.github.io/libica/openapi/v2/docs/AnalysisOutput>`_]

    :raises: ApiException

    :Examples:

    .. code-block:: python
    
        :linenos:
        from wrapica.project_analysis import get_outputs_object_from_analysis_id

        # Set params
        project_id = "project_id"
        analysis_id = "analysis_id"

        workflow_outputs = get_outputs_object_from_analysis_id(project_id, analysis_id)
    """
    # Enter a context with an instance of the API client
    with ApiClient(get_icav2_configuration()) as api_client:
        # Create an instance of the API class
        api_instance = ProjectAnalysisApi(api_client)

    # example passing only required values which don't have defaults set
    try:
        # Retrieve the outputs of an analysis
        api_response: AnalysisOutputList = api_instance.get_analysis_outputs(project_id, analysis_id)
    except ApiException as e:
        logger.error("Exception when calling ProjectAnalysisApi->get_analysis_outputs: %s\n" % e)
        raise ApiException

    return api_response.items


def get_analysis_output_object_from_analysis_output_code(
    project_id: str,
    analysis_id: str,
    analysis_output_code: str
) -> AnalysisOutput:
    """
    Given an analysis code for an analysis id, collect the analysis output object

    :param project_id: The project context the analysis was run in
    :param analysis_id: The analysis id to query
    :param analysis_output_code: The analysis output code to collect

    :return: The analysis output object
    :rtype: `AnalysisOutput <https://umccr-illumina.github.io/libica/openapi/v2/docs/AnalysisOutput>`_

    :raises: StopIteration, ValueError, ApiException

    :Examples:

    .. code-block:: python

        :linenos:
        from wrapica.project_analysis import get_analysis_output_object_from_analysis_code

        # Set params
        project_id = "project_id"
        analysis_id = "analysis_id"
        analysis_code = "Output"

        analysis_folder_output_id = get_analysis_output_object_from_analysis_code(
            project_id, analysis_id, analysis_code
        ).data[0].data_id
    """
    analysis_output: List[AnalysisOutput] = get_outputs_object_from_analysis_id(
        project_id,
        analysis_id
    )
    try:
        output_obj: AnalysisOutput = next(
            filter(
                lambda analysis_iter: analysis_iter.code == analysis_output_code,
                analysis_output
            )
        )
    except StopIteration:
        logger.error(f"Could not get output item from analysis {analysis_id}")
        raise StopIteration

    if len(output_obj.data) == 0:
        logger.error(f"Expected analysis output data to be at least 1 but got {len(output_obj.data)}")
        raise ValueError

    return output_obj


def get_cwl_outputs_json_from_analysis_id(
    project_id: str,
    analysis_id: str
) -> Dict[str, Any]:
    """
    Query the outputs object from the analysis id

    :param project_id: The project context the analysis was run in
    :param analysis_id: The analysis id to query

    :return: List of analysis outputs
    :rtype: Dict[str, Any]

    :raises: ApiException

    :Examples:

    .. code-block:: python

        :linenos:
        from wrapica.project_analysis import get_cwl_outputs_json_from_analysis_id

        # Set params
        project_id = "project_id"
        analysis_id = "analysis_id"

        cwl_json_output = get_cwl_outputs_json_from_analysis_id(project_id, analysis_id)
    """
    # Enter a context with an instance of the API client
    with ApiClient(get_icav2_configuration()) as api_client:
        # Create an instance of the API class
        api_instance = ProjectAnalysisApi(api_client)

    # example passing only required values which don't have defaults set
    try:
        # Retrieve the outputs of an analysis
        api_response: CwlAnalysisOutputJson = api_instance.get_cwl_output_json(
            project_id, analysis_id
        )
    except ApiException as e:
        logger.error("Exception when calling ProjectAnalysisApi->get_analysis_outputs: %s\n" % e)
        raise ApiException

    return json.loads(api_response.output_json)


def get_analysis_obj_from_analysis_id(
    project_id: str,
    analysis_id: str
) -> AnalysisType:
    """
    Get an analysis object given a project id and analysis id

    :param project_id: The project context the analysis was run in
    :param analysis_id: The analysis id to query

    :return: The analysis object
    :rtype: `Analysis <https://umccr-illumina.github.io/libica/openapi/v2/docs/Analysis>`_

    :raises: ApiException

    :Examples:

    .. code-block:: python

        :linenos:
        from wrapica.project_analysis import get_analysis_obj_from_analysis_id

        # Set params
        project_id = "project_id"
        analysis_id = "analysis_id"

        analysis = get_analysis_obj_from_analysis_id(project_id, analysis_id)
    """
    # Enter a context with an instance of the API client
    with ApiClient(get_icav2_configuration()) as api_client:
        # Create an instance of the API class
        api_instance = ProjectAnalysisApi(api_client)

    # example passing only required values which don't have defaults set
    try:
        # Retrieve an analysis.
        api_response: AnalysisType = api_instance.get_analysis(project_id, analysis_id)
    except ApiException as e:
        logger.error("Exception when calling ProjectAnalysisApi->get_analysis: %s\n" % e)
        raise ApiException

    return api_response


def get_analysis_steps(
    project_id: str,
    analysis_id: str,
    include_technical_steps: bool = False
) -> List[AnalysisStep]:
    """
    Get the workflow steps for a given analysis

    :param project_id:
    :param analysis_id:
    :param include_technical_steps:

    :return: List of analysis steps
    :rtype: List[`AnalysisStep <https://umccr-illumina.github.io/libica/openapi/v2/docs/AnalysisStep>`_]

    :raises: ApiException

    :Examples:

    .. code-block:: python

        :linenos:
        from wrapica.project_analysis import get_analysis_steps

        # Set params
        project_id = "project_id"
        analysis_id = "analysis_id"

        analysis_step_list = get_analysis_steps(project_id, analysis_id)
    """
    # Enter a context with an instance of the API client
    with ApiClient(get_icav2_configuration()) as api_client:
        # Create an instance of the API class
        api_instance = ProjectAnalysisApi(api_client)

    # example passing only required values which don't have defaults set
    try:
        # Retrieve the individual steps of an analysis.
        api_response = api_instance.get_analysis_steps(
            project_id,
            analysis_id
        )
    except ApiException as e:
        logger.error("Exception when calling ProjectAnalysisApi->get_analysis_steps: %s\n" % e)
        raise ApiException

    # Collect all steps
    analysis_steps: List[AnalysisStep] = api_response.items

    if not include_technical_steps:
        analysis_steps = list(
            filter(
                lambda step_iter: step_iter.technical is False,
                analysis_steps
            )
        )

    return analysis_steps


def get_analysis_log_from_analysis_step(
    analysis_step: AnalysisStep
) -> AnalysisStepLogs:
    """
    Get the logs for a given analysis step

    :param analysis_step:

    :return: Get the logs attribute of an analysis step
    :rtype: `AnalysisStepLogs <https://umccr-illumina.github.io/libica/openapi/v2/docs/AnalysisStepLogs>`_

    :Examples:

    .. code-block:: python

        :linenos:
        from wrapica.project_analysis import get_analysis_steps

        # Set params
        project_id = "project_id"
        analysis_id = "analysis_id"

        analysis_step_list = get_analysis_steps(project_id, analysis_id)

        step_logs = list(map(get_analysis_log_from_analysis_step, analysis_step_list))
    """
    return analysis_step.logs


def write_analysis_step_logs(
    project_id: str,
    step_logs: AnalysisStepLogs,
    log_name: AnalysisLogStreamName,
    output_path: Union[Path | TextIOWrapper],
    is_cwltool_log: Optional[bool] = False
) -> None:
    """
    Write the analysis step logs to a file

    :param project_id: The project id the analysis was run in
    :param step_logs: Required, the step logs object
    :param log_name: Required, One of stdout or stderr
    :param output_path: Required, The output path to write the file to
    :param is_cwltool_log: Required, If the log is a cwltool log we convert from html to text format

    :raises: ApiException, NotADirectoryError

    :return: None
    :rtype: None

    :Examples:

    .. code-block:: python

        :linenos:
        from pathlib import Path
        from wrapica.project_analysis import get_analysis_steps

        # Set params
        project_id = "project_id"
        analysis_id = "analysis_id"

        # Get analysis steps and logs
        analysis_step_list = get_analysis_steps(project_id, analysis_id)
        step_logs = list(map(get_analysis_log_from_analysis_step, analysis_step_list))

        # Write first step logs to file
        write_analysis_step_logs(
            project_id=project_id,
            step_logs=step_logs[0],
            log_name="stderr",
            output_path=Path("stderr.log")
        )

    """
    # Local imports
    from ...project_data import read_icav2_file_contents

    # Check if we're getting our log from a stream
    is_stream = False
    log_stream = None
    log_data_id = ""

    # Initialise list of non empty log attributes
    non_empty_log_attrs = []

    # Check attributes of log obj
    for attr in dir(step_logs):
        if attr.startswith('_'):
            continue
        if getattr(step_logs, attr) is None:
            continue
        non_empty_log_attrs.append(attr)

    if log_name == "stdout":
        if hasattr(step_logs, "std_out_stream") and step_logs.std_out_stream is not None:
            is_stream = True
            log_stream = step_logs.std_out_stream
        elif hasattr(step_logs, "std_out_data") and step_logs.std_out_data is not None:
            log_data_id: str = step_logs.std_out_data.id
        else:
            logger.error("Could not get either file output or stream of logs")
            logger.error(f"The available attributes were {', '.join(non_empty_log_attrs)}")
            raise AttributeError
    else:
        if hasattr(step_logs, "std_err_stream") and step_logs.std_err_stream is not None:
            is_stream = True
            log_stream = step_logs.std_err_stream
        elif hasattr(step_logs, "std_err_data") and step_logs.std_err_data is not None:
            log_data_id: str = step_logs.std_err_data.id
        else:
            logger.error("Could not get either file output or stream of logs")
            logger.error(f"The available attributes were {', '.join(non_empty_log_attrs)}")
            raise AttributeError
    if is_stream:
        if is_cwltool_log:
            temp_html_obj = NamedTemporaryFile()
            write_websocket_to_file(
                log_stream,
                output_file=Path(temp_html_obj.name)
            )
            convert_html_to_text(Path(temp_html_obj.name), output_path)
        else:
            write_websocket_to_file(
                log_stream,
                output_file=output_path
            )
    else:
        read_icav2_file_contents(project_id, log_data_id, output_path)


def abort_analysis(
    project_id: str,
    analysis_id: str,
) -> None:
    """
    Abort an analysis

    :param project_id: The project id the analysis was run in
    :param analysis_id: The analysis id to abort

    :raises: ApiException

    :return: None
    :rtype: None

    :Examples:

    .. code-block:: python

        :linenos:
        from pathlib import Path
        from wrapica.project_analysis import abort_analysis

        # Set params
        # Use wrapica.project.get_project_id_from_project_name
        # If you need to convert a project_name to a project_id
        project_id = "project_id"
        analysis_id = "analysis_id"
    """
    # Get the configuration
    configuration = get_icav2_configuration()

    # Enter a context with an instance of the API client
    with ApiClient(configuration) as api_client:
        # Force default headers for endpoints with a ':' in the name
        api_client.set_default_header(
            header_name="Content-Type",
            header_value="application/vnd.illumina.v3+json"
        )
        api_client.set_default_header(
            header_name="Accept",
            header_value="application/vnd.illumina.v3+json"
        )
        # Create an instance of the API class
        api_instance = ProjectAnalysisApi(api_client)

    # example passing only required values which don't have defaults set
    try:
        # Abort an analysis.
        api_instance.abort_analysis(project_id, analysis_id)
    except ApiException as e:
        logger.error("Exception when calling ProjectAnalysisApi->abort_analysis: %s\n" % e)
        raise ApiException


def list_analyses(
    project_id: str,
    pipeline_id: Optional[str] = None,
    user_reference: Optional[str] = None,
    status: Optional[Union[ProjectAnalysisStatus, List[ProjectAnalysisStatus]]] = None,
    creation_date_before: Optional[datetime] = None,
    creation_date_after: Optional[datetime] = None,
    modification_date_before: Optional[datetime] = None,
    modification_date_after: Optional[datetime] = None,
    sort: Optional[Union[ProjectAnalysisSortParameters, List[ProjectAnalysisSortParameters]]] = None,
    max_items: Optional[int] = None
) -> List[AnalysisV4]:
    """
    List analyses

    :param project_id: The project id
    :param pipeline_id: The pipeline id
    :param user_reference: The full user reference (regex optional)
    :param status: The status of the analysis
    :param creation_date_before: Return only analyses created before this date
    :param creation_date_after: Return only analyses created after this date
    :param modification_date_before: Return only analyses modified before this date
    :param modification_date_after: Return only analyses modified after this date
    :param sort: A parameter, or list of parameters to sort by
    :param max_items:

    :raises: ApiException

    :return: List of analyses
    :rtype: `List[Analysis] <https://umccr-illumina.github.io/libica/openapi/v2/docs/AnalysisV4/>`_

    :Examples:

    .. code-block:: python

        :linenos:
        from pathlib import Path
        from wrapica.project_analysis import list_analyses

        # Set params
        # Use wrapica.project.get_project_id_from_project_name
        # If you need to convert a project_name to a project_id
        project_id = "project_id"

        analysis_list = list_analyses(project_id)
    """
    # Get the configuration
    configuration = get_icav2_configuration()

    # Check parameters
    if user_reference is not None and IS_REGEX_MATCH.search(user_reference) is not None:
        user_reference_regex = re.compile(user_reference)
        user_reference = None
    else:
        user_reference_regex = None

    if isinstance(status, ProjectAnalysisStatus):
        status = [status]

    if sort is not None:
        if isinstance(sort, ProjectAnalysisSortParameters):
            sort = [sort]
        elif isinstance(sort, str):
            sort = [ProjectAnalysisSortParameters(sort)]

        # Complete a comma join of the sort parameters
        sort = ",".join([sort_param.value for sort_param in sort])

    # AnalysisQueryParameters
    analysis_query_parameters = {
        "status": list(map(lambda status_iter: status_iter.value, status)) if status is not None else None,
        "user_reference": user_reference,
    }
    analysis_query_parameters = AnalysisQueryParameters(
        # Filter out None values
        **{k: v for k, v in analysis_query_parameters.items() if v is not None}
    )

    # Enter a context with an instance of the API client
    with ApiClient(configuration) as api_client:
        # Force default headers for endpoints with a ':' in the name
        api_client.set_default_header(
            header_name="Content-Type",
            header_value="application/vnd.illumina.v3+json"
        )
        api_client.set_default_header(
            header_name="Accept",
            header_value="application/vnd.illumina.v3+json"
        )
        # Create an instance of the API class
        api_instance = ProjectAnalysisApi(api_client)

    # Set page parameters
    if max_items is None:
        max_items = 0
    if not max_items == 0 and max_items < LIBICAV2_DEFAULT_PAGE_SIZE:
        page_size = max_items
    else:
        page_size = LIBICAV2_DEFAULT_PAGE_SIZE
    page_token = ""
    # We use page tokens if sort is None, otherwise we use page offsets
    if sort is not None:
        page_offset = 0
    else:
        page_offset = ""

    # Initialise list
    analysis_list: List[AnalysisV4] = []

    # Loop through the pages
    while True:
        # Attempt to collect all analyses
        try:
            api_response = api_instance.search_analyses(
                **dict(
                    filter(
                        lambda x: x[1] is not None,
                        {
                            "status": status,
                            "project_id": project_id,
                            "page_size": str(page_size),
                            "page_offset": str(page_offset),
                            "page_token": page_token,
                            "analysis_query_parameters": analysis_query_parameters,
                            "sort": sort
                        }.items()
                    )
                )
            )
        except ApiException as e:
            raise ValueError("Exception when calling ProjectDataApi->get_project_data_list: %s\n" % e)

        # Extend items list
        analysis_list.extend(api_response.items)

        # Determine page iteration method by if we have a 'sort' parameter
        if sort is not None:
            # Check page offset and page size against total item count
            if page_offset + page_size >= api_response.total_item_count:
                break
            if not max_items == 0 and len(analysis_list) >= max_items:
                break

            # Continuing iteration
            page_offset += page_size
            # Add page size to page offset
            # But check if we're approaching the max_items
            if not max_items == 0 and len(analysis_list) + page_size > max_items:
                page_size = max_items - len(analysis_list)
        else:
            # Check if there is a next page
            if api_response.next_page_token is None or api_response.next_page_token == "":
                break
            page_token = api_response.next_page_token

    # Before we return the list, we filter
    # pipeline_id
    # creation_date_after
    # creation_date_before
    # modification_date_after
    # modification_date_before
    analysis_list = list(
        filter(
            lambda analysis_iter: (
                (
                    pipeline_id is None or
                    analysis_iter.pipeline.id == pipeline_id
                ) and
                (
                    user_reference_regex is None or
                    user_reference_regex.match(analysis_iter.user_reference) is not None
                ) and
                (
                    creation_date_after is None or
                    analysis_iter.creation_date >= creation_date_after
                ) and
                (
                    creation_date_before is None or
                    analysis_iter.creation_date <= creation_date_before
                ) and
                (
                    modification_date_after is None or
                    analysis_iter.modification_date >= modification_date_after
                ) and
                (
                    modification_date_before is None or
                    analysis_iter.modification_date <= modification_date_before
                )
            ),
            analysis_list
        )
    )

    return analysis_list


def get_cwl_analysis_input_json(
    project_id: str,
    analysis_id: str
) -> Dict:
    """
    Get the CWL Analysis Input JSON

    :param project_id: The project id the analysis was run in
    :param analysis_id:  The analysis id

    :return: The CWL Analysis Input JSON As a dictionary
    :rtype: Dict

    :raises: ApiException

    :Examples:

    .. code-block:: python
        :linenos:

        # Imports
        import json
        from wrapica.project_analysis import get_cwl_analysis_input_json

        # Set params
        project_id = "project_id"
        analysis_id = "analysis_id"

        # Print the input json
        print(
            json.dumps(
                get_cwl_analysis_input_json(project_id, analysis_id),
                indent=4
            )
        )

    """
    with ApiClient(get_icav2_configuration()) as api_client:
        # Create an instance of the API class
        api_instance = ProjectAnalysisApi(api_client)

    try:
        # Retrieve the input json of a CWL analysis.
        api_response: CwlAnalysisInputJson = api_instance.get_cwl_input_json(project_id, analysis_id)
    except ApiException as e:
        logger.error("Exception when calling ProjectAnalysisApi->get_cwl_input_json: %s\n" % e)
        raise ApiException

    return json.loads(api_response.input_json)


def get_cwl_analysis_output_json(project_id: str, analysis_id: str) -> Dict:
    """
    Get the CWL Analysis Input JSON

    :param project_id: The project id the analysis was run in
    :param analysis_id: The analysis id

    :return: The CWL Analysis Output JSON As a dictionary
    :rtype: Dict

    :raises: ApiException

    :Examples:

    .. code-block:: python
        :linenos:

        # Imports
        import json
        from wrapica.project_analysis import get_cwl_analysis_output_json

        # Set params
        project_id = "project_id"
        analysis_id = "analysis_id"

        # Print the output json
        print(
            json.dumps(
                get_cwl_analysis_output_json(project_id, analysis_id),
                indent=4
            )
        )

    """
    with ApiClient(get_icav2_configuration()) as api_client:
        # Create an instance of the API class
        api_instance = ProjectAnalysisApi(api_client)

    try:
        # Retrieve the input json of a CWL analysis.
        api_response: CwlAnalysisOutputJson = api_instance.get_cwl_output_json(project_id, analysis_id)
    except ApiException as e:
        logger.error("Exception when calling ProjectAnalysisApi->get_cwl_output_json: %s\n" % e)
        raise ApiException

    return json.loads(api_response.output_json)


def analysis_step_to_dict(analysis_step: AnalysisStep) -> Dict:
    """
    Convert an analysis step object to a dictionary

    Returns the following attributes
    * name: str -> remove preceding '#' values
    * status: WorkflowStep.Status -> The workflow step status
    * queue_date: Datetime object
    * start_date: Datetime object
    * end_date: Datetime object

    :param analysis_step: The AnalysisStep object
    :return: The analysis step as a dict
    :rtype: Dict
    """
    return {
        "name": analysis_step.name.split("#", 1)[-1],
        "status": ProjectAnalysisStepStatus(analysis_step.status),
        "queue_date": analysis_step.queue_date if hasattr(analysis_step, "queue_date") else None,
        "start_date": analysis_step.start_date if hasattr(analysis_step, "start_date") else None,
        "end_date": analysis_step.end_date if hasattr(analysis_step, "end_date") else None
    }


def get_analysis_obj_from_user_reference(
    project_id: str,
    user_reference: str
) -> AnalysisType:
    """
    Given a user reference, get the analysis object

    Will fail if more than one analysis is found for a given user reference.
    Will also fail if no analysis is found for the user reference.

    :param project_id:
    :param user_reference:
    :return:
    """

    # List analysis filtering on the user reference
    # List analysis filtering on the user reference
    analysis_list = list_analyses(
        project_id=project_id,
        user_reference=user_reference
    )

    if len(analysis_list) == 0:
        logger.error("Could not find analysis id from user reference")
        raise ValueError

    if not len(analysis_list) == 1:
        logger.error(
            f"Got {len(analysis_list)} analyses from user reference, "
            f"cannot coerce user reference to analysis id"
        )
        raise ValueError

    return analysis_list[0]


def coerce_analysis_id_or_user_reference_to_analysis_obj(
    project_id: str,
    analysis_id_or_user_reference: str
) -> AnalysisType:
    """
    Given either an analysis id or user reference, coerce to an analysis object

    :param project_id:
    :param analysis_id_or_user_reference:
    :return: The analysis object
    :rtype: `Analysis <https://umccr-illumina.github.io/libica/openapi/v2/docs/Analysis>`_

    :raises: ValueError

    :Examples:

    .. code-block:: python

        :linenos:
        from wrapica.project_analysis import coerce_analysis_id_or_user_reference_to_analysis_obj

        # Set params
        analysis_id_or_user_reference = "analysis_id"

        analysis = coerce_analysis_id_or_user_reference_to_analysis_obj(analysis_id_or_user_reference)
    """

    # For an analysis id
    if is_uuid_format(analysis_id_or_user_reference):
        return get_analysis_obj_from_analysis_id(
            project_id=project_id,
            analysis_id=analysis_id_or_user_reference
        )

    return get_analysis_obj_from_user_reference(
        project_id=project_id,
        user_reference=analysis_id_or_user_reference
    )


def coerce_analysis_id_or_user_reference_to_analysis_id(
    project_id: str,
    analysis_id_or_user_reference: str
) -> str:
    """
    Given either an analysis id or user reference, coerce to an analysis id

    :param project_id:
    :param analysis_id_or_user_reference:
    :return:
    """

    if is_uuid_format(analysis_id_or_user_reference):
        return analysis_id_or_user_reference

    return get_analysis_obj_from_user_reference(
        project_id=project_id,
        user_reference=analysis_id_or_user_reference
    ).id
