# Copyright (c) 2021, Qu & Co
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright notice, this
#    list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
This module contains all the public methods of the qubec-sdk API covering all most important use cases such as
authenticating to the QUBEC platform, launching quantum jobs and retrieving previously completed job information.
"""
# built-in
from binascii import Error as EncodingError
from dataclasses import asdict
from typing import Union
# local
from . import PROJECT_VERSION as VERSION
from .client import QubecClient, get_client
from .job import QubecJob
from .parameters import (
    AVAILABLE_ALGORITHMS,
    AVAILABLE_BACKEND_TYPES,
    AVAILABLE_PROPERTIES,
    DEFAULT_BACKEND_TYPE,
    DEFAULT_N_SHOTS,
    DEFAULT_PROVIDER,
    DEFAULT_QPU_CHIP,
    DEFAULT_QUANTUM_PARAMETERS,
    DEFAULT_SCF_PARAMETERS,
    DEFAULT_SIMULATION_TYPE,
    QubecAlgorithm,
    QubecChemistryProblem,
    QubecSdkError,
)
from .tools import encode_geometry_xyz
[docs]def enable_account(username: str,
                   password: str = None,
                   token: str = None) -> dict:
    """
    Login into a QUBEC account providing valid user credentials. This function
    must be called before starting any QUBEC session otherwise the execution
    of quantum jobs will not be allowed
    Args:
        username (str): The username as registered on the QUBEC platform
        password (str): The password associated with the username. If no password
            is provided, then a valid token must be supplied instead
        token (str): An optional previously retrieved JWT token to use for login
    Returns:
        A dictionary with token information. Beware that this might contain a password
            in clear text, do not store it on disk
    Raises:
        QubecSdkError if credentials are invalid or the backed call did not succeed
    """
    client: QubecClient = get_client()
    if password is not None:
        client.login(username, password)
    elif token is not None:
        client.current_user(token)
    else:
        raise QubecSdkError("Provide either password or a valid QUBEC token")
    return asdict(client.token) 
[docs]def execute(
    problem: dict = None,
    algorithm_type: Union[str, QubecAlgorithm] = None,
    backend_type: str = DEFAULT_BACKEND_TYPE,
    provider: str = DEFAULT_PROVIDER,
    simulation_type: str = DEFAULT_SIMULATION_TYPE,
    n_shots: int = DEFAULT_N_SHOTS,
    qpu_chip: str = DEFAULT_QPU_CHIP,
    scf_parameters: dict = DEFAULT_SCF_PARAMETERS,
    quantum_parameters: dict = DEFAULT_QUANTUM_PARAMETERS,
) -> QubecJob:
    """
    Execute a new quantum job on the QUBEC platform. If the QUBEC token in the current session
    has expired, this function will automatically refresh it
    Args:
        problem (dict): A dictionary containing the problem definition which must contain
            at least 'geometry' and 'basis_set' keys. A full example of this dictionary is:
        ```
        problem = {
            "geometry": [
                ("H", (0.0, 0.0, 0.0)),
                ("H", (0.5, 0.5, 0.5))
            ],
            "basis_set": "sto-3g",
            "charge": 1
        }
        ```
        algorithm_type (Union[str, QubecAlgorithm]): The type of quantum algorithm to execute. Currently
            only 'vqa' and 'qpe_re' are allowed values
        backend_type (str): The backend type where the algorithm needs to be executed. Currently one
            can choose among 'simulator', 'noisy_simulator' and 'qpu'
        provider (str): The quantum computing provider to use
        simulation_type (str): If the algorithm selected is 'vqa', this string contains the type of quantum
            simulation to execute chosen between 'wavefunction' or 'tomography'
        n_shots (int): The number of circuit repetitions, used only if 'tomography' is selected as simulation type
        qpu_chip (str): If 'qpu' is selected as backend_type, this parameter specifies the actual quantum chip to use
        scf_parameters (dict): the parameters of the self-consistent preprocessing calculation. See qb_sdk.parameters documentation
            for information on the valid parameters
        quantum_parameters (dict): the parameters of the quantum job. See qb_sdk.parameters documentation for information
            on the valid parameters
    Returns:
        A QubecJob instance with a unique job identifier created by the QUBEC platform and all the relevant
            information to retrieve job progress later
    Raises:
        QubecSdk if some parameters are wrong or the job has failed to be submitted correctly
    """
    client: QubecClient = get_client()
    client.refresh_session()
    try:
        problem = problem if problem is not None else {}
        problem_model = QubecChemistryProblem(**problem)
        problem_model.geometry = encode_geometry_xyz(problem_model.geometry)
        properties = list(problem_model.properties.keys())
        are_valid_props = all(
            prop in AVAILABLE_PROPERTIES for prop in properties)
        if len(properties) > 0 and not are_valid_props:
            raise QubecSdkError(
                f"Choose among the following physical properties: {AVAILABLE_PROPERTIES}"
            )
    except EncodingError:
        raise QubecSdkError(f"Wrong problem definition provided: {problem}")
    except QubecSdkError as e:
        raise e
    if isinstance(algorithm_type, QubecAlgorithm):
        algorithm_type = algorithm_type.name
    if algorithm_type is None or algorithm_type not in AVAILABLE_ALGORITHMS:
        raise QubecSdkError(
            f"Choose among the following quantum algorithms: {AVAILABLE_ALGORITHMS}"
        )
    if backend_type not in AVAILABLE_BACKEND_TYPES:
        raise QubecSdkError(
            f"Choose among the following backend types: {AVAILABLE_BACKEND_TYPES}"
        )
    data = {
        "algorithm_type": algorithm_type,
        "backend_type": backend_type,
        "backend": {
            "provider": provider,
            "simulation_type": simulation_type,
            "n_shots": n_shots,
            "chip": qpu_chip,
        },
        "problem_definition": asdict(problem_model),
        "scf_parameters": scf_parameters,
        "quantum_parameters": quantum_parameters,
    }
    content = client.submit_job(data)
    job = QubecJob(
        content["data"]["job_id"],
        experiment=data["algorithm_type"],
        backend_type=data["backend_type"],
        backend=data["backend"],
        problem=data["problem_definition"],
        **scf_parameters,
        **quantum_parameters,
    )
    return job 
[docs]def new_default_session() -> QubecClient:
    """
    Logout from the current QUBEC session and begin a new one
    """
    client: QubecClient = get_client()
    client.logout()
    return client 
[docs]def version() -> str:
    """
    Get the current package version
    """
    return VERSION 
# TODO
[docs]def get_job(job_id: str) -> QubecJob:
    """
    Deserialize and existing QUBEC job into a new QubecJob instance
    Args:
        job_id (str): The unique identifier of the job
    Returns:
        A populated instance of QubecJob
    """
    pass