import math
from binascii import hexlify

from clvm_tools import binutils

from src.types.condition_opcodes import ConditionOpcode
from src.types.program import Program
from src.types.sized_bytes import bytes32
from src.util.ints import uint64


def rl_puzzle_for_pk(
    pubkey: bytes,
    rate_amount: uint64,
    interval_time: uint64,
    origin_id: bytes32,
    clawback_pk: bytes,
):
    """
        Solution to this puzzle must be in format:
        (1 my_parent_id, my_puzzlehash, my_amount, outgoing_puzzle_hash, outgoing_amount,
         min_block_time, parent_parent_id, parent_amount)
        RATE LIMIT LOGIC:
        M - chia_per_interval
        N - interval_blocks
        V - amount being spent
        MIN_BLOCK_AGE = V / (M / N)
        if not (min_block_age * M >=  V * N) do X (raise)
        ASSERT_COIN_BLOCK_AGE_EXCEEDS min_block_age
    """

    hex_pk = pubkey.hex()
    clawback_pk_str = clawback_pk.hex()
    origin_id = origin_id.hex()

    opcode_aggsig = ConditionOpcode.AGG_SIG.hex()
    opcode_coin_block_age = ConditionOpcode.ASSERT_BLOCK_AGE_EXCEEDS.hex()
    opcode_create = ConditionOpcode.CREATE_COIN.hex()
    opcode_myid = ConditionOpcode.ASSERT_MY_COIN_ID.hex()

    TEMPLATE_MY_PARENT_ID = "(sha256 (f (r (r (r (r (r (r (a)))))))) (f (r (a))) (f (r (r (r (r (r (r (r (a))))))))))"
    TEMPLATE_SINGLETON_RL = f'((c (i (i (= {TEMPLATE_MY_PARENT_ID} (f (a))) (q 1) (= (f (a)) (q 0x{origin_id}))) (q ()) (q (x (q "Parent doesnt satisfy RL conditions")))) (a)))'  # noqa: E501
    TEMPLATE_BLOCK_AGE = f'((c (i (i (= (* (f (r (r (r (r (r (a))))))) (q {rate_amount})) (* (f (r (r (r (r (a)))))) (q {interval_time}))) (q 1) (q (> (* (f (r (r (r (r (r (a))))))) (q {rate_amount})) (* (f (r (r (r (r (a))))))) (q {interval_time})))) (q (c (q 0x{opcode_coin_block_age}) (c (f (r (r (r (r (r (a))))))) (q ())))) (q (x (q "wrong min block time")))) (a) ))'  # noqa: E501
    TEMPLATE_MY_ID = f"(c (q 0x{opcode_myid}) (c (sha256 (f (a)) (f (r (a))) (f (r (r (a))))) (q ())))"  # noqa: E501
    CREATE_CHANGE = f"(c (q 0x{opcode_create}) (c (f (r (a))) (c (- (f (r (r (a)))) (f (r (r (r (r (a))))))) (q ()))))"  # noqa: E501
    CREATE_NEW_COIN = f"(c (q 0x{opcode_create}) (c (f (r (r (r (a))))) (c (f (r (r (r (r (a)))))) (q ()))))"  # noqa: E501
    RATE_LIMIT_PUZZLE = f"(c {TEMPLATE_SINGLETON_RL} (c {TEMPLATE_BLOCK_AGE} (c {CREATE_CHANGE} (c {TEMPLATE_MY_ID} (c {CREATE_NEW_COIN} (q ()))))))"  # noqa: E501

    TEMPLATE_MY_PARENT_ID_2 = "(sha256 (f (r (r (r (r (r (r (r (r (a)))))))))) (f (r (a))) (f (r (r (r (r (r (r (r (a))))))))))"  # noqa: E501
    TEMPLATE_SINGLETON_RL_2 = f'((c (i (i (= {TEMPLATE_MY_PARENT_ID_2} (f (r (r (r (r (r (a)))))))) (q 1) (= (f (r (r (r (r (r (a))))))) (q 0x{origin_id}))) (q (c (q 1) (q ()))) (q (x (q "Parent doesnt satisfy RL conditions")))) (a)))'  # noqa: E501
    CREATE_CONSOLIDATED = f"(c (q 0x{opcode_create}) (c (f (r (a))) (c (+ (f (r (r (r (r (a)))))) (f (r (r (r (r (r (r (a))))))))) (q ()))))"  # noqa: E501
    MODE_TWO_ME_STRING = f"(c (q 0x{opcode_myid}) (c (sha256 (f (r (r (r (r (r (a))))))) (f (r (a))) (f (r (r (r (r (r (r (a))))))))) (q ())))"  # noqa: E501
    CREATE_LOCK = f"(c (q 0x{opcode_create}) (c (sha256tree (c (q 7) (c (c (q 5) (c (c (q 1) (c (sha256 (f (r (r (a)))) (f (r (r (r (a))))) (f (r (r (r (r (a))))))) (q ()))) (c (q (q ())) (q ())))) (q ())))) (c (q 0) (q ()))))"  # noqa: E501

    MODE_TWO = f"(c {TEMPLATE_SINGLETON_RL_2} (c {MODE_TWO_ME_STRING} (c {CREATE_LOCK} (c {CREATE_CONSOLIDATED} (q ())))))"  # noqa: E501

    AGGSIG_ENTIRE_SOLUTION = (
        f"(c (q 0x{opcode_aggsig}) (c (q 0x{hex_pk}) (c (sha256tree (a)) (q ()))))"
    )

    WHOLE_PUZZLE = f"(c {AGGSIG_ENTIRE_SOLUTION} ((c (i (= (f (a)) (q 1)) (q ((c (q {RATE_LIMIT_PUZZLE}) (r (a))))) (q {MODE_TWO})) (a))) (q ()))"  # noqa: E501
    CLAWBACK = f"(c (c (q 0x{opcode_aggsig}) (c (q 0x{clawback_pk_str}) (c (sha256tree (a)) (q ())))) (r (a)))"
    WHOLE_PUZZLE_WITH_CLAWBACK = (
        f"((c (i (= (f (a)) (q 3)) (q {CLAWBACK}) (q {WHOLE_PUZZLE})) (a)))"
    )
    return Program(binutils.assemble(WHOLE_PUZZLE_WITH_CLAWBACK))


def rl_make_aggregation_solution(myid, wallet_coin_primary_input, wallet_coin_amount):
    opcode_myid = hexlify(myid).decode("ascii")
    primary_input = hexlify(wallet_coin_primary_input).decode("ascii")
    sol = f"(0x{opcode_myid} 0x{primary_input} {wallet_coin_amount})"
    return Program(binutils.assemble(sol))


def make_clawback_solution(puzzlehash, amount):
    opcode_create = hexlify(ConditionOpcode.CREATE_COIN).decode("ascii")
    solution = f"(3 (0x{opcode_create} 0x{puzzlehash} {amount}))"
    return Program(binutils.assemble(solution))


def rl_make_solution_mode_2(
    my_puzzle_hash,
    consolidating_primary_input,
    consolidating_coin_puzzle_hash,
    outgoing_amount,
    my_primary_input,
    incoming_amount,
    parent_amount,
    my_parent_parent_id,
):
    my_puzzle_hash = hexlify(my_puzzle_hash).decode("ascii")
    consolidating_primary_input = hexlify(consolidating_primary_input).decode("ascii")
    consolidating_coin_puzzle_hash = hexlify(consolidating_coin_puzzle_hash).decode(
        "ascii"
    )
    primary_input = hexlify(my_primary_input).decode("ascii")
    sol = f"(2 0x{my_puzzle_hash} 0x{consolidating_primary_input} 0x{consolidating_coin_puzzle_hash} {outgoing_amount} 0x{primary_input} {incoming_amount} {parent_amount} 0x{my_parent_parent_id})"  # noqa: E501
    return Program(binutils.assemble(sol))


def solution_for_rl(
    my_parent_id: bytes32,
    my_puzzlehash: bytes32,
    my_amount: uint64,
    out_puzzlehash: bytes32,
    out_amount: uint64,
    my_parent_parent_id: bytes32,
    parent_amount: uint64,
    interval,
    limit,
):
    """
    Solution is (1 my_parent_id, my_puzzlehash, my_amount, outgoing_puzzle_hash, outgoing_amount,
    min_block_time, parent_parent_id, parent_amount)
    min block time = Math.ceil((new_amount * self.interval) / self.limit)
    """

    min_block_count = math.ceil((out_amount * interval) / limit)
    solution = (
        f"(1 0x{my_parent_id.hex()} 0x{my_puzzlehash.hex()} {my_amount} 0x{out_puzzlehash.hex()} {out_amount}"
        f" {min_block_count} 0x{my_parent_parent_id.hex()} {parent_amount})"
    )
    return Program(binutils.assemble(solution))


def rl_make_aggregation_puzzle(wallet_puzzle):
    """
     If Wallet A wants to send further funds to Wallet B then they can lock them up using this code
     Solution will be (my_id wallet_coin_primary_input wallet_coin_amount)
    """
    opcode_myid = hexlify(ConditionOpcode.ASSERT_MY_COIN_ID).decode("ascii")
    opcode_consumed = hexlify(ConditionOpcode.ASSERT_COIN_CONSUMED).decode("ascii")
    me_is_my_id = f"(c (q 0x{opcode_myid}) (c (f (a)) (q ())))"

    # lock_puzzle is the hash of '(r (c (q "merge in ID") (q ())))'
    lock_puzzle = "(sha256tree (c (q 7) (c (c (q 5) (c (c (q 1) (c (f (a)) (q ()))) (c (q (q ())) (q ())))) (q ()))))"
    parent_coin_id = f"(sha256 (f (r (a))) (q 0x{wallet_puzzle}) (f (r (r (a)))))"
    input_of_lock = f"(c (q 0x{opcode_consumed}) (c (sha256 {parent_coin_id} {lock_puzzle} (q 0)) (q ())))"
    puz = f"(c {me_is_my_id} (c {input_of_lock} (q ())))"

    return Program(binutils.assemble(puz))
