# Copyright (C) 2024 qBraid
#
# This file is part of the qBraid-SDK
#
# The qBraid-SDK is free software released under the GNU General Public License v3
# or later. You can redistribute and/or modify it under the terms of the GPL v3.
# See the LICENSE file in the project root or <https://www.gnu.org/licenses/gpl-3.0.html>.
#
# THERE IS NO WARRANTY for the qBraid-SDK, as per Section 15 of the GPL v3.

"""
Module for transpiling quantum programs between different quantum programming languages

"""

from __future__ import annotations

import warnings
from copy import deepcopy
from typing import TYPE_CHECKING, Any, Callable, Optional

from qbraid_core._import import LazyLoader

from qbraid._logging import logger
from qbraid.programs import QPROGRAM_ALIASES
from qbraid.programs.alias_manager import (
    _get_program_type_alias,
    get_program_type_alias,
)

from .exceptions import (
    ConversionPathNotFoundError,
    NodeNotFoundError,
    ProgramConversionError,
)
from .graph import ConversionGraph, _get_path_from_bound_methods

if TYPE_CHECKING:
    import qbraid.programs


def _warn_if_unsupported(program_type, program_direction):
    if program_type not in QPROGRAM_ALIASES:
        warnings.warn(
            f"Converting {program_direction} unsupported program type '{program_type}'.",
            UserWarning,
        )


def _format_exception(err: Exception) -> str:
    return f"{type(err).__name__}: {str(err)}\n"


def transpile(
    program: qbraid.programs.QPROGRAM,
    target: str,
    conversion_graph: Optional[ConversionGraph] = None,
    max_path_attempts: int = 3,
    max_path_depth: Optional[int] = None,
    **kwargs,
) -> qbraid.programs.QPROGRAM:
    """
    Transpile a quantum program to a target language using a conversion graph.
    This function attempts to find a conversion path from the program's current
    format to the target format. It can limit the search to a certain number of
    attempts and path depths.

    Args:
        program (qbraid.programs.QPROGRAM): The quantum program to transpile.
        target (str): The target language to transpile to.
        conversion_graph (Optional[ConversionGraph]): The graph representing available conversions.
            If None, a default graph is used. Defaults to None.
        max_path_attempts (int): The maximum number of conversion paths to attempt before raising an
            exception. This is useful to avoid excessive computations when multiple paths are
            available. Defaults to 3.
        max_path_depth (Optional[int]): The maximum depth of conversions within a given path to
            allow. For example, a path with a depth of 2 would be ['cirq' -> 'qasm2' -> 'qiskit'],
            whereas a depth  of 1 would be a direct conversion ['cirq' -> 'braket']. Defaults
            to None, i.e. no limit set on the path depth.

    Returns:
        qbraid.programs.QPROGRAM: The transpiled quantum program.

    Raises:
        NodeNotFoundError: If the target or source package is not in the ConversionGraph.
        ConversionPathNotFoundError: If no path is available to conversion between the
            source and target packages.
        ProgramConversionError: If the conversion fails through all attempted paths.
    """
    graph = conversion_graph or ConversionGraph(**kwargs)
    graph_type = "Default" if conversion_graph is None else "Provided"

    if not graph.has_node(target):
        raise NodeNotFoundError(graph_type, target, graph.nodes())

    source = _get_program_type_alias(program)

    if not graph.has_node(source):
        raise NodeNotFoundError(graph_type, source, graph.nodes())

    if not graph.has_path(source, target):
        raise ConversionPathNotFoundError(source, target)

    if source == target:
        return program

    _warn_if_unsupported(source, "from")
    _warn_if_unsupported(target, "to")

    paths = graph.find_top_shortest_conversion_paths(source, target, top_n=max_path_attempts)

    if max_path_depth is not None:
        paths = [path for path in paths if len(path) <= max_path_depth]
        if len(paths) == 0:
            raise ConversionPathNotFoundError(source, target, max_path_depth)

    error_messages = []

    for path in paths:
        path_details = _get_path_from_bound_methods(path)
        try:
            temp_program = deepcopy(program)
        except (RecursionError, TypeError) as err:
            logger.info(
                "Deepcopy failed due to a %s, likely caused by the internal structure of "
                "the %s object. Continuing execution, but any subsequent errors during "
                "transpilation may be unclear or misleading due to potential side effects.",
                type(err).__name__,
                type(program),
            )
            temp_program = program
        try:
            for convert_func in path:
                try:
                    temp_program = convert_func(temp_program)
                except Exception as err:  # pylint: disable=broad-exception-caught
                    alias = get_program_type_alias(temp_program, safe=True)

                    if alias == "cirq":
                        cirq_qasm_import = LazyLoader(
                            "cirq_qasm_import", globals(), "cirq.contrib.qasm_import"
                        )
                        qasm: str = temp_program.to_qasm()  # type: ignore[attr-defined]
                        temp_program = cirq_qasm_import.circuit_from_qasm(qasm)
                        temp_program = convert_func(temp_program)  # Retry conversion
                    else:
                        error_detail = (
                            f"Conversion {path_details} failed due to "
                            f"exception raised while converting from '{alias}'."
                        )
                        error_messages.append(error_detail)
                        error_messages.append(_format_exception(err))
                        raise

            logger.info("Successfully transpiled using conversions: %s", path_details)
            return temp_program
        except Exception as err:  # pylint: disable=broad-exception-caught
            logger.info("Failed to transpile using conversions: %s", path_details)
            formatted_error = _format_exception(err)
            if len(error_messages) == 0 or error_messages[-1] != formatted_error:
                error_messages.append(formatted_error)
            continue

    raise ProgramConversionError(
        f"Failed to convert '{source}' to '{target}'"
        + (
            " due to the following error(s):\n\n" + "\n".join(error_messages)
            if error_messages
            else "."
        )
    )


def chain_calls(func: Callable[[Any, Any], Any], initial_value, *args, **kwargs) -> Any:
    """
    Apply a function iteratively over a sequence of arguments.

    Args:
        func (Callable[[Any, Any], Any]): The function to apply.
        initial_value (Any): The initial input to the first call.
        *args (Any): The sequence of arguments for subsequent calls.
        **kwargs (Any): Additional keyword arguments for the function.

    Returns:
        Any: The result of the final call.
    """
    result = initial_value
    for arg in args:
        result = func(result, arg, **kwargs)
    return result


def translate(program, *targets, **kwargs):
    """
    Chain multiple transpile calls on a program.

    Args:
        program: The quantum program to transpile.
        *targets: A sequence of target formats.

    Returns:
        The final transpiled program.
    """
    return chain_calls(transpile, program, *targets, **kwargs)
