# This file is part of the Trezor project.
#
# Copyright (C) SatoshiLabs and contributors
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version 3
# as published by the Free Software Foundation.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the License along with this library.
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.

import json
from typing import TYPE_CHECKING, Any, Optional, TextIO

import click

from .. import cardano, messages, tools
from . import ChoiceType, with_session

if TYPE_CHECKING:
    from ..client import Session

PATH_HELP = "BIP-32 path to key, e.g. m/44h/1815h/0h/0/0"

TESTNET_CHOICES = {
    "preprod": "testnet_preprod",
    "preview": "testnet_preview",
    "legacy": "testnet_legacy",
}


@click.group(name="cardano")
def cli() -> None:
    """Cardano commands."""


@cli.command()
@click.argument("file", type=click.File("r"))
@click.option("-f", "--file", "_ignore", is_flag=True, hidden=True, expose_value=False)
@click.option(
    "-s",
    "--signing-mode",
    required=True,
    type=ChoiceType({m.name: m for m in messages.CardanoTxSigningMode}),
)
@click.option(
    "-p", "--protocol-magic", type=int, default=cardano.PROTOCOL_MAGICS["mainnet"]
)
@click.option("-N", "--network-id", type=int, default=cardano.NETWORK_IDS["mainnet"])
@click.option("-t", "--testnet", type=ChoiceType(TESTNET_CHOICES))
@click.option(
    "-D",
    "--derivation-type",
    type=ChoiceType({m.name: m for m in messages.CardanoDerivationType}),
    default=messages.CardanoDerivationType.ICARUS,
)
@click.option("-i", "--include-network-id", is_flag=True)
@click.option("-C", "--chunkify", is_flag=True)
@click.option("-T", "--tag-cbor-sets", is_flag=True)
@with_session(cardano=True)
def sign_tx(
    session: "Session",
    file: TextIO,
    signing_mode: messages.CardanoTxSigningMode,
    protocol_magic: int,
    network_id: int,
    testnet: str,
    derivation_type: messages.CardanoDerivationType,
    include_network_id: bool,
    chunkify: bool,
    tag_cbor_sets: bool,
) -> cardano.SignTxResponse:
    """Sign Cardano transaction."""
    transaction = json.load(file)

    if testnet:
        protocol_magic = cardano.PROTOCOL_MAGICS[testnet]
        network_id = cardano.NETWORK_IDS["testnet"]

    inputs = [cardano.parse_input(input) for input in transaction["inputs"]]
    outputs = [cardano.parse_output(output) for output in transaction["outputs"]]
    fee = transaction["fee"]
    ttl = transaction.get("ttl")
    validity_interval_start = transaction.get("validity_interval_start")
    certificates = [
        cardano.parse_certificate(certificate)
        for certificate in transaction.get("certificates", ())
    ]
    withdrawals = [
        cardano.parse_withdrawal(withdrawal)
        for withdrawal in transaction.get("withdrawals", ())
    ]
    auxiliary_data = cardano.parse_auxiliary_data(transaction.get("auxiliary_data"))
    mint = cardano.parse_mint(transaction.get("mint", ()))
    script_data_hash = cardano.parse_script_data_hash(
        transaction.get("script_data_hash")
    )
    collateral_inputs = [
        cardano.parse_collateral_input(collateral_input)
        for collateral_input in transaction.get("collateral_inputs", ())
    ]
    required_signers = [
        cardano.parse_required_signer(required_signer)
        for required_signer in transaction.get("required_signers", ())
    ]
    collateral_return = (
        cardano.parse_output(transaction["collateral_return"])
        if transaction.get("collateral_return")
        else None
    )
    total_collateral = transaction.get("total_collateral")
    reference_inputs = [
        cardano.parse_reference_input(reference_input)
        for reference_input in transaction.get("reference_inputs", ())
    ]
    additional_witness_requests = [
        cardano.parse_additional_witness_request(p)
        for p in transaction["additional_witness_requests"]
    ]

    sign_tx_response = cardano.sign_tx(
        session,
        signing_mode,
        inputs,
        outputs,
        fee,
        ttl,
        validity_interval_start,
        certificates,
        withdrawals,
        protocol_magic,
        network_id,
        auxiliary_data,
        mint,
        script_data_hash,
        collateral_inputs,
        required_signers,
        collateral_return,
        total_collateral,
        reference_inputs,
        additional_witness_requests,
        derivation_type=derivation_type,
        include_network_id=include_network_id,
        chunkify=chunkify,
        tag_cbor_sets=tag_cbor_sets,
    )

    sign_tx_response["tx_hash"] = sign_tx_response["tx_hash"].hex()
    sign_tx_response["witnesses"] = [
        {
            "type": witness["type"],
            "pub_key": witness["pub_key"].hex(),
            "signature": witness["signature"].hex(),
            "chain_code": (
                witness["chain_code"].hex()
                if witness["chain_code"] is not None
                else None
            ),
        }
        for witness in sign_tx_response["witnesses"]
    ]
    auxiliary_data_supplement = sign_tx_response.get("auxiliary_data_supplement")
    if auxiliary_data_supplement:
        auxiliary_data_supplement["auxiliary_data_hash"] = auxiliary_data_supplement[
            "auxiliary_data_hash"
        ].hex()
        cvote_registration_signature = auxiliary_data_supplement.get(
            "cvote_registration_signature"
        )
        if cvote_registration_signature:
            auxiliary_data_supplement["cvote_registration_signature"] = (
                cvote_registration_signature.hex()
            )
        sign_tx_response["auxiliary_data_supplement"] = auxiliary_data_supplement
    return sign_tx_response


@cli.command()
@click.option("-n", "--address", type=str, default="", help=PATH_HELP)
@click.option("-d", "--show-display", is_flag=True)
@click.option(
    "-t",
    "--address-type",
    type=ChoiceType({m.name: m for m in messages.CardanoAddressType}),
    default="BASE",
)
@click.option("-s", "--staking-address", type=str, default="")
@click.option("-h", "--staking-key-hash", type=str, default=None)
@click.option("-b", "--block_index", type=int, default=None)
@click.option("-x", "--tx_index", type=int, default=None)
@click.option("-c", "--certificate_index", type=int, default=None)
@click.option("--script-payment-hash", type=str, default=None)
@click.option("--script-staking-hash", type=str, default=None)
@click.option(
    "-p", "--protocol-magic", type=int, default=cardano.PROTOCOL_MAGICS["mainnet"]
)
@click.option("-N", "--network-id", type=int, default=cardano.NETWORK_IDS["mainnet"])
@click.option("-e", "--testnet", type=ChoiceType(TESTNET_CHOICES))
@click.option(
    "-D",
    "--derivation-type",
    type=ChoiceType({m.name: m for m in messages.CardanoDerivationType}),
    default=messages.CardanoDerivationType.ICARUS,
)
@click.option("-C", "--chunkify", is_flag=True)
@with_session(cardano=True)
def get_address(
    session: "Session",
    address: str,
    address_type: messages.CardanoAddressType,
    staking_address: str,
    staking_key_hash: Optional[str],
    block_index: Optional[int],
    tx_index: Optional[int],
    certificate_index: Optional[int],
    script_payment_hash: Optional[str],
    script_staking_hash: Optional[str],
    protocol_magic: int,
    network_id: int,
    show_display: bool,
    testnet: str,
    derivation_type: messages.CardanoDerivationType,
    chunkify: bool,
) -> str:
    """
    Get Cardano address.

    All address types require the address, address_type, protocol_magic and
    network_id parameters.

    When deriving a base address you can choose to include staking info as
    staking_address or staking_key_hash - one has to be chosen.

    When deriving a pointer address you need to specify the block_index,
    tx_index and certificate_index parameters.

    Byron, enterprise and reward addresses only require the general parameters.
    """
    if testnet:
        protocol_magic = cardano.PROTOCOL_MAGICS[testnet]
        network_id = cardano.NETWORK_IDS["testnet"]

    staking_key_hash_bytes = cardano.parse_optional_bytes(staking_key_hash)
    script_payment_hash_bytes = cardano.parse_optional_bytes(script_payment_hash)
    script_staking_hash_bytes = cardano.parse_optional_bytes(script_staking_hash)

    address_parameters = cardano.create_address_parameters(
        address_type,
        tools.parse_path(address),
        tools.parse_path(staking_address),
        staking_key_hash_bytes,
        block_index,
        tx_index,
        certificate_index,
        script_payment_hash_bytes,
        script_staking_hash_bytes,
    )

    return cardano.get_address(
        session,
        address_parameters,
        protocol_magic,
        network_id,
        show_display,
        derivation_type=derivation_type,
        chunkify=chunkify,
    )


@cli.command()
@click.option("-n", "--address", required=True, help=PATH_HELP)
@click.option(
    "-D",
    "--derivation-type",
    type=ChoiceType({m.name: m for m in messages.CardanoDerivationType}),
    default=messages.CardanoDerivationType.ICARUS,
)
@click.option("-d", "--show-display", is_flag=True)
@with_session(cardano=True)
def get_public_key(
    session: "Session",
    address: str,
    derivation_type: messages.CardanoDerivationType,
    show_display: bool,
) -> messages.CardanoPublicKey:
    """Get Cardano public key."""
    address_n = tools.parse_path(address)
    return cardano.get_public_key(
        session, address_n, derivation_type=derivation_type, show_display=show_display
    )


@cli.command()
@click.argument("file", type=click.File("r"))
@click.option(
    "-d",
    "--display-format",
    type=ChoiceType({m.name: m for m in messages.CardanoNativeScriptHashDisplayFormat}),
    default="HIDE",
)
@click.option(
    "-D",
    "--derivation-type",
    type=ChoiceType({m.name: m for m in messages.CardanoDerivationType}),
    default=messages.CardanoDerivationType.ICARUS,
)
@with_session(cardano=True)
def get_native_script_hash(
    session: "Session",
    file: TextIO,
    display_format: messages.CardanoNativeScriptHashDisplayFormat,
    derivation_type: messages.CardanoDerivationType,
) -> messages.CardanoNativeScriptHash:
    """Get Cardano native script hash."""
    native_script_json = json.load(file)
    native_script = cardano.parse_native_script(native_script_json)

    return cardano.get_native_script_hash(
        session, native_script, display_format, derivation_type=derivation_type
    )


@cli.command()
@click.argument("file", type=click.File("r"))
@click.option(
    "-D",
    "--derivation-type",
    type=ChoiceType({m.name: m for m in messages.CardanoDerivationType}),
    default=messages.CardanoDerivationType.ICARUS,
)
@with_session(cardano=True)
def sign_message(
    session: "Session",
    file: TextIO,
    derivation_type: messages.CardanoDerivationType,
) -> messages.CardanoMessageSignature:
    """Sign Cardano message containing arbitrary data."""
    request: dict[Any, Any] = json.load(file)

    return cardano.sign_message(
        session,
        payload=bytes.fromhex(request["payload"]),
        prefer_hex_display=request["prefer_hex_display"],
        signing_path=tools.parse_path(request["signing_path"]),
        address_parameters=cardano.parse_optional_address_parameters(
            request.get("address_parameters")
        ),
        protocol_magic=request.get("protocol_magic"),
        network_id=request.get("network_id"),
        derivation_type=derivation_type,
    )
