"""Library to interface with the system program."""
from __future__ import annotations

from typing import Any, NamedTuple, Union

from solana import sysvar
from solana._layouts.system_instructions import SYSTEM_INSTRUCTIONS_LAYOUT, InstructionType
from solana.publickey import PublicKey
from solana.transaction import AccountMeta, Transaction, TransactionInstruction
from solana.utils.validate import validate_instruction_keys, validate_instruction_type

SYS_PROGRAM_ID: PublicKey = PublicKey("11111111111111111111111111111111")
"""Public key that identifies the System program."""


# Instruction Params
class CreateAccountParams(NamedTuple):
    """Create account system transaction params."""

    from_pubkey: PublicKey
    """"""
    new_account_pubkey: PublicKey
    """"""
    lamports: int
    """"""
    space: int
    """"""
    program_id: PublicKey
    """"""


class TransferParams(NamedTuple):
    """Transfer system transaction params."""

    from_pubkey: PublicKey
    """"""
    to_pubkey: PublicKey
    """"""
    lamports: int
    """"""


class AssignParams(NamedTuple):
    """Assign system transaction params."""

    account_pubkey: PublicKey
    """"""
    program_id: PublicKey
    """"""


class CreateAccountWithSeedParams(NamedTuple):
    """Create account with seed system transaction params."""

    from_pubkey: PublicKey
    """"""
    new_account_pubkey: PublicKey
    """"""
    base_pubkey: PublicKey
    """"""
    seed: str
    """"""
    lamports: int
    """"""
    space: int
    """"""
    program_id: PublicKey
    """"""


class CreateNonceAccountParams(NamedTuple):
    """Create nonce account system transaction params."""

    from_pubkey: PublicKey
    """"""
    nonce_pubkey: PublicKey
    """"""
    authorized_pubkey: PublicKey
    """"""
    lamports: int
    """"""


class CreateNonceAccountWithSeedParams(NamedTuple):
    """Create nonce account with seed system transaction params."""

    from_pubkey: PublicKey
    """"""
    nonce_pubkey: PublicKey
    """"""
    authorized_pubkey: PublicKey
    """"""
    lamports: int
    """"""
    base_pubkey: PublicKey
    """"""
    seed: str
    """"""


class InitializeNonceParams(NamedTuple):
    """Initialize nonce account system instruction params."""

    nonce_pubkey: PublicKey
    """"""
    authorized_pubkey: PublicKey
    """"""


class AdvanceNonceParams(NamedTuple):
    """Advance nonce account system instruction params."""

    nonce_pubkey: PublicKey
    """"""
    authorized_pubkey: PublicKey
    """"""


class WithdrawNonceParams(NamedTuple):
    """Withdraw nonce account system transaction params."""

    nonce_pubkey: PublicKey
    """"""
    authorized_pubkey: PublicKey
    """"""
    to_pubkey: PublicKey
    """"""
    lamports: int
    """"""


class AuthorizeNonceParams(NamedTuple):
    """Authorize nonce account system transaction params."""

    nonce_pubkey: PublicKey
    """"""
    authorized_pubkey: PublicKey
    """"""
    new_authorized_pubkey: PublicKey
    """"""


class AllocateParams(NamedTuple):
    """Allocate account with seed system transaction params."""

    account_pubkey: PublicKey
    """"""
    space: int
    """"""


class AllocateWithSeedParams(NamedTuple):
    """Allocate account with seed system transaction params."""

    account_pubkey: PublicKey
    """"""
    base_pubkey: PublicKey
    """"""
    seed: str
    """"""
    space: int
    """"""
    program_id: PublicKey
    """"""


class AssignWithSeedParams(NamedTuple):
    """Assign account with seed system transaction params."""

    account_pubkey: PublicKey
    """"""
    base_pubkey: PublicKey
    """"""
    seed: str
    """"""
    program_id: PublicKey
    """"""


def __check_program_id(program_id: PublicKey) -> None:
    if program_id != SYS_PROGRAM_ID:
        raise ValueError("invalid instruction: programId is not SystemProgram")


def __parse_and_validate_instruction(
    instruction: TransactionInstruction,
    expected_keys: int,
    expected_type: InstructionType,
) -> Any:  # Returns a Construct container.
    validate_instruction_keys(instruction, expected_keys)
    data = SYSTEM_INSTRUCTIONS_LAYOUT.parse(instruction.data)
    validate_instruction_type(data, expected_type)
    return data


def decode_create_account(instruction: TransactionInstruction) -> CreateAccountParams:
    """Decode a create account system instruction and retrieve the instruction params.

    Args:
        instruction: The instruction to decode.

    Example:

        >>> from solana.publickey import PublicKey
        >>> from_account, new_account, program_id = PublicKey(1), PublicKey(2), PublicKey(3)
        >>> instruction = create_account(
        ...     CreateAccountParams(
        ...         from_pubkey=from_account, new_account_pubkey=new_account,
        ...         lamports=1, space=1, program_id=program_id)
        ... )
        >>> decode_create_account(instruction)
        CreateAccountParams(from_pubkey=11111111111111111111111111111112, new_account_pubkey=11111111111111111111111111111113, lamports=1, space=1, program_id=11111111111111111111111111111114)

    Returns:
        The decoded instruction params.
    """  # noqa: E501 # pylint: disable=line-too-long
    parsed_data = __parse_and_validate_instruction(instruction, 2, InstructionType.CREATE_ACCOUNT)
    return CreateAccountParams(
        from_pubkey=instruction.keys[0].pubkey,
        new_account_pubkey=instruction.keys[1].pubkey,
        lamports=parsed_data.args.lamports,
        space=parsed_data.args.space,
        program_id=PublicKey(parsed_data.args.program_id),
    )


def decode_transfer(instruction: TransactionInstruction) -> TransferParams:
    """Decode a transfer system instruction and retrieve the instruction params.

    Args:
        instruction: The instruction to decode.

    Example:

        >>> from solana.publickey import PublicKey
        >>> sender, receiver = PublicKey(1), PublicKey(2)
        >>> instruction = transfer(
        ...     TransferParams(from_pubkey=sender, to_pubkey=receiver, lamports=1000)
        ... )
        >>> decode_transfer(instruction)
        TransferParams(from_pubkey=11111111111111111111111111111112, to_pubkey=11111111111111111111111111111113, lamports=1000)

    Returns:
        The decoded instruction params.
    """  # pylint: disable=line-too-long # noqa: E501
    parsed_data = __parse_and_validate_instruction(instruction, 2, InstructionType.TRANSFER)
    return TransferParams(
        from_pubkey=instruction.keys[0].pubkey, to_pubkey=instruction.keys[1].pubkey, lamports=parsed_data.args.lamports
    )


def decode_allocate(instruction: TransactionInstruction) -> AllocateParams:
    """Decode an allocate system instruction and retrieve the instruction params.

    Args:
        instruction: The instruction to decode.

    Example:

        >>> from solana.publickey import PublicKey
        >>> allocator = PublicKey(1)
        >>> instruction = allocate(
        ...     AllocateParams(account_pubkey=allocator,space=65537)
        ... )
        >>> decode_allocate(instruction)
        AllocateParams(account_pubkey=11111111111111111111111111111112, space=65537)

    Returns:
        The decoded instruction params.
    """  # pylint: disable=line-too-long # noqa: E501
    parsed_data = __parse_and_validate_instruction(instruction, 1, InstructionType.ALLOCATE)
    return AllocateParams(account_pubkey=instruction.keys[0].pubkey, space=parsed_data.args.space)


def decode_allocate_with_seed(instruction: TransactionInstruction) -> AllocateWithSeedParams:
    """Decode an allocate with seed system instruction and retrieve the instruction params.

    Args:
        instruction: The instruction to decode.

    Example:

        >>> from solana.publickey import PublicKey
        >>> allocator, base, program_id = PublicKey(1), PublicKey(2), PublicKey(3)
        >>> instruction = allocate(
        ...     AllocateWithSeedParams(
        ...         account_pubkey=allocator,
        ...         base_pubkey=base,
        ...         seed={"length": 4, "chars": "gqln"},
        ...         space=65537,
        ...         program_id=program_id
        ...     )
        ... )
        >>> decode_allocate_with_seed(instruction)
        AllocateWithSeedParams(account_pubkey=11111111111111111111111111111112, base_pubkey=11111111111111111111111111111113, seed=Container(length=4, chars=u'gqln'), space=65537, program_id=11111111111111111111111111111114)

    Returns:
        The decoded instruction params.
    """  # pylint: disable=line-too-long # noqa: E501
    parsed_data = __parse_and_validate_instruction(instruction, 1, InstructionType.ALLOCATE_WITH_SEED)
    return AllocateWithSeedParams(
        account_pubkey=instruction.keys[0].pubkey,
        base_pubkey=PublicKey(parsed_data.args.base),
        seed=parsed_data.args.seed,
        space=parsed_data.args.space,
        program_id=PublicKey(parsed_data.args.program_id),
    )


def decode_assign(instruction: TransactionInstruction) -> AssignParams:
    """Decode an assign system instruction and retrieve the instruction params.

    Args:
        instruction: The instruction to decode.

    Example:

        >>> from solana.publickey import PublicKey
        >>> account, program_id = PublicKey(1), PublicKey(2)
        >>> instruction = assign(
        ...     AssignParams(account_pubkey=account, program_id=program_id)
        ... )
        >>> decode_assign(instruction)
        AssignParams(account_pubkey=11111111111111111111111111111112, program_id=11111111111111111111111111111113)

    Returns:
        The decoded instruction params.
    """
    parsed_data = __parse_and_validate_instruction(instruction, 1, InstructionType.ASSIGN)
    return AssignParams(account_pubkey=instruction.keys[0].pubkey, program_id=PublicKey(parsed_data.args.program_id))


def decode_assign_with_seed(instruction: TransactionInstruction) -> AssignWithSeedParams:
    """Decode an assign system with seed instruction and retrieve the instruction params."""
    raise NotImplementedError("decode_assign_with_seed not implemented")


def decode_create_account_with_seed(instruction: TransactionInstruction) -> CreateAccountWithSeedParams:
    """Decode a create account with seed system instruction and retrieve the instruction params.

    Args:
        instruction: The instruction to decode.

    Returns:
        The decoded instruction params.
    """
    parsed_data = __parse_and_validate_instruction(instruction, 1, InstructionType.CREATE_ACCOUNT_WITH_SEED)
    return CreateAccountWithSeedParams(
        from_pubkey=instruction.keys[0].pubkey,
        new_account_pubkey=instruction.keys[1].pubkey,
        base_pubkey=PublicKey(parsed_data.args.base),
        seed=parsed_data.args.seed,
        lamports=parsed_data.args.lamports,
        space=parsed_data.args.space,
        program_id=PublicKey(parsed_data.args.program_id),
    )


def decode_nonce_initialize(instruction: TransactionInstruction) -> InitializeNonceParams:
    """Decode a nonce initialize system instruction and retrieve the instruction params."""
    raise NotImplementedError("decode_nonce_initialize not implemented")


def decode_nonce_advance(instruction: TransactionInstruction) -> AdvanceNonceParams:
    """Decode a nonce advance system instruction and retrieve the instruction params."""
    raise NotImplementedError("decode_nonce_advance not implemented")


def decode_nonce_withdraw(instruction: TransactionInstruction) -> WithdrawNonceParams:
    """Decode a nonce withdraw system instruction and retrieve the instruction params."""
    raise NotImplementedError("decode_nonce_withdraw not implemented")


def decode_nonce_authorize(instruction: TransactionInstruction) -> AuthorizeNonceParams:
    """Decode a nonce authorize system instruction and retrieve the instruction params."""
    raise NotImplementedError("decode_nonce_authorize not implemented")


def create_account(params: CreateAccountParams) -> TransactionInstruction:
    """Generate an instruction that creates a new account.

    Args:
        params: The create account params.

    Example:
        >>> from solana.publickey import PublicKey
        >>> from_account, new_account, program_id = PublicKey(1), PublicKey(2), PublicKey(3)
        >>> instruction = create_account(
        ...     CreateAccountParams(
        ...         from_pubkey=from_account, new_account_pubkey=new_account,
        ...         lamports=1, space=1, program_id=program_id)
        ... )
        >>> type(instruction)
        <class 'solana.transaction.TransactionInstruction'>

    Returns:
        The instruction to create the account.
    """
    data = SYSTEM_INSTRUCTIONS_LAYOUT.build(
        dict(
            instruction_type=InstructionType.CREATE_ACCOUNT,
            args=dict(lamports=params.lamports, space=params.space, program_id=bytes(params.program_id)),
        )
    )

    return TransactionInstruction(
        keys=[
            AccountMeta(pubkey=params.from_pubkey, is_signer=True, is_writable=True),
            AccountMeta(pubkey=params.new_account_pubkey, is_signer=True, is_writable=True),
        ],
        program_id=SYS_PROGRAM_ID,
        data=data,
    )


def assign(params: Union[AssignParams, AssignWithSeedParams]) -> TransactionInstruction:
    """Generate an instruction that assigns an account to a program.

    Args:
        params: The assign params.

    Example:
        >>> from solana.publickey import PublicKey
        >>> account, program_id = PublicKey(1), PublicKey(2)
        >>> instruction = assign(
        ...     AssignParams(account_pubkey=account, program_id=program_id)
        ... )
        >>> type(instruction)
        <class 'solana.transaction.TransactionInstruction'>
    """
    if isinstance(params, AssignWithSeedParams):
        raise NotImplementedError("assign with key is not implemented")
    data = SYSTEM_INSTRUCTIONS_LAYOUT.build(
        dict(instruction_type=InstructionType.ASSIGN, args=dict(program_id=bytes(params.program_id)))
    )

    return TransactionInstruction(
        keys=[
            AccountMeta(pubkey=params.account_pubkey, is_signer=True, is_writable=True),
        ],
        program_id=SYS_PROGRAM_ID,
        data=data,
    )


def transfer(params: TransferParams) -> TransactionInstruction:
    """Generate an instruction that transfers lamports from one account to another.

    Args:
        params: The transfer params.

    Example:

        >>> from solana.publickey import PublicKey
        >>> sender, receiver = PublicKey(1), PublicKey(2)
        >>> instruction = transfer(
        ...     TransferParams(from_pubkey=sender, to_pubkey=receiver, lamports=1000)
        ... )
        >>> type(instruction)
        <class 'solana.transaction.TransactionInstruction'>

    Returns:
        The transfer instruction.

    """
    data = SYSTEM_INSTRUCTIONS_LAYOUT.build(
        dict(instruction_type=InstructionType.TRANSFER, args=dict(lamports=params.lamports))
    )

    return TransactionInstruction(
        keys=[
            AccountMeta(pubkey=params.from_pubkey, is_signer=True, is_writable=True),
            AccountMeta(pubkey=params.to_pubkey, is_signer=False, is_writable=True),
        ],
        program_id=SYS_PROGRAM_ID,
        data=data,
    )


def create_account_with_seed(
    params: CreateAccountWithSeedParams,
) -> TransactionInstruction:
    """Generate a instruction that creates a new account at an address generated with `from`, a seed, and programId.

    Args:
        params: account creation params.

    Returns:
        The instruction to create the account.
    """
    data = SYSTEM_INSTRUCTIONS_LAYOUT.build(
        dict(
            instruction_type=InstructionType.CREATE_ACCOUNT_WITH_SEED,
            args=dict(
                base=bytes(params.base_pubkey),
                seed=params.seed,
                lamports=params.lamports,
                space=params.space,
                program_id=bytes(params.program_id),
            ),
        )
    )

    keys = [
        AccountMeta(pubkey=params.from_pubkey, is_signer=True, is_writable=True),
        AccountMeta(pubkey=params.new_account_pubkey, is_signer=False, is_writable=True),
    ]

    if params.base_pubkey != params.from_pubkey:
        keys.append(AccountMeta(pubkey=params.base_pubkey, is_signer=True, is_writable=False))

    return TransactionInstruction(keys=keys, program_id=SYS_PROGRAM_ID, data=data)


def create_nonce_account(params: Union[CreateNonceAccountParams, CreateNonceAccountWithSeedParams]) -> Transaction:
    """Generate a Transaction that creates a new Nonce account.

    Args:
        params: The create nonce params.

    Returns:
        The transaction to create the new nonce account.
    """
    if isinstance(params, CreateNonceAccountParams):
        create_account_instruction = create_account(
            CreateAccountParams(
                from_pubkey=params.from_pubkey,
                new_account_pubkey=params.nonce_pubkey,
                lamports=params.lamports,
                space=80,  # derived from rust implementation
                program_id=SYS_PROGRAM_ID,
            )
        )
    else:
        create_account_instruction = create_account_with_seed(
            CreateAccountWithSeedParams(
                from_pubkey=params.from_pubkey,
                new_account_pubkey=params.nonce_pubkey,
                base_pubkey=params.base_pubkey,
                seed=params.seed,
                lamports=params.lamports,
                space=80,  # derived from rust implementation
                program_id=SYS_PROGRAM_ID,
            )
        )

    initialize_nonce_instruction = nonce_initialization(
        InitializeNonceParams(
            nonce_pubkey=params.nonce_pubkey,
            authorized_pubkey=params.authorized_pubkey,
        )
    )

    return Transaction(fee_payer=params.from_pubkey).add(create_account_instruction, initialize_nonce_instruction)


def nonce_initialization(params: InitializeNonceParams) -> TransactionInstruction:
    """Generate an instruction to initialize a Nonce account.

    Args:
        params: The nonce initialization params.

    Returns:
        The instruction to initialize the nonce account.

    """
    data = SYSTEM_INSTRUCTIONS_LAYOUT.build(
        dict(
            instruction_type=InstructionType.INITIALIZE_NONCE_ACCOUNT,
            args=dict(
                authorized=bytes(params.authorized_pubkey),
            ),
        )
    )

    return TransactionInstruction(
        keys=[
            AccountMeta(pubkey=params.nonce_pubkey, is_signer=True, is_writable=True),
            AccountMeta(pubkey=sysvar.SYSVAR_RECENT_BLOCKHASHES_PUBKEY, is_signer=False, is_writable=False),
            AccountMeta(pubkey=sysvar.SYSVAR_RENT_PUBKEY, is_signer=False, is_writable=False),
        ],
        program_id=SYS_PROGRAM_ID,
        data=data,
    )


def nonce_advance(params: AdvanceNonceParams) -> TransactionInstruction:
    """Generate an instruction to advance the nonce in a Nonce account.

    Args:
        params: The advance nonce params

    Returns:
        The instruction to advance the nonce.
    """
    data = SYSTEM_INSTRUCTIONS_LAYOUT.build(
        dict(
            instruction_type=InstructionType.ADVANCE_NONCE_ACCOUNT,
            args={},
        )
    )

    return TransactionInstruction(
        keys=[
            AccountMeta(pubkey=params.nonce_pubkey, is_signer=False, is_writable=True),
            AccountMeta(pubkey=sysvar.SYSVAR_RECENT_BLOCKHASHES_PUBKEY, is_signer=False, is_writable=False),
            AccountMeta(pubkey=params.authorized_pubkey, is_signer=True, is_writable=True),
        ],
        program_id=SYS_PROGRAM_ID,
        data=data,
    )


def nonce_withdraw(params: WithdrawNonceParams) -> TransactionInstruction:
    """Generate an instruction that withdraws lamports from a Nonce account."""
    raise NotImplementedError("nonce_withdraw not implemented")


def nonce_authorize(params: AuthorizeNonceParams) -> TransactionInstruction:
    """Generate an instruction that authorizes a new PublicKey as the authority on a Nonce account."""
    raise NotImplementedError("nonce_authorize not implemented")


def allocate(params: Union[AllocateParams, AllocateWithSeedParams]) -> TransactionInstruction:
    """Generate an instruction that allocates space in an account without funding.

    Args:
        params: The allocate params.

    Example:

        >>> from solana.publickey import PublicKey
        >>> allocator = PublicKey(1)
        >>> instruction = allocate(
        ...     AllocateParams(account_pubkey=allocator, space=65537)
        ... )
        >>> type(instruction)
        <class 'solana.transaction.TransactionInstruction'>

    Returns:
        The allocate instruction.
    """
    if isinstance(params, AllocateWithSeedParams):
        data = SYSTEM_INSTRUCTIONS_LAYOUT.build(
            dict(
                instruction_type=InstructionType.ALLOCATE_WITH_SEED,
                args=dict(
                    base=bytes(params.base_pubkey),
                    seed=params.seed,
                    space=params.space,
                    program_id=bytes(params.program_id),
                ),
            )
        )
    else:
        data = SYSTEM_INSTRUCTIONS_LAYOUT.build(
            dict(instruction_type=InstructionType.ALLOCATE, args=dict(space=params.space))
        )

    return TransactionInstruction(
        keys=[
            AccountMeta(pubkey=params.account_pubkey, is_signer=True, is_writable=True),
        ],
        program_id=SYS_PROGRAM_ID,
        data=data,
    )
