from __future__ import annotations

import cProfile
import random
import sqlite3
import sys
import time
from contextlib import closing, contextmanager
from pathlib import Path
from typing import Iterator, List, Optional

import click
import zstd

from chik.simulator.block_tools import create_block_tools
from chik.simulator.keyring import TempKeyring
from chik.types.blockchain_format.coin import Coin
from chik.types.spend_bundle import SpendBundle
from chik.util.chik_logging import initialize_logging
from chik.util.ints import uint32, uint64
from tools.test_constants import test_constants


@contextmanager
def enable_profiler(profile: bool, counter: int) -> Iterator[None]:
    if not profile:
        yield
        return

    if sys.version_info < (3, 8):
        raise Exception(f"Python 3.8 or higher required when profiling is requested, running with: {sys.version}")

    with cProfile.Profile() as pr:
        yield

    pr.create_stats()
    pr.dump_stats(f"generate-chain-{counter}.profile")


@click.command()
@click.option("--length", type=int, default=None, required=False, help="the number of blocks to generate")
@click.option(
    "--fill-rate",
    type=int,
    default=100,
    required=False,
    help="the transaction fill rate of blocks. Specified in percent of max block cost",
)
@click.option("--profile", is_flag=True, required=False, default=False, help="dump CPU profile at the end")
@click.option(
    "--block-refs",
    type=bool,
    required=False,
    default=True,
    help="include a long list of block references in each transaction block",
)
@click.option(
    "--output", type=str, required=False, default=None, help="the filename to write the resulting sqlite database to"
)
def main(length: int, fill_rate: int, profile: bool, block_refs: bool, output: Optional[str]) -> None:
    if fill_rate < 0 or fill_rate > 100:
        print("fill-rate must be within [0, 100]")
        sys.exit(1)

    if not length:
        if block_refs:
            # we won't have full reflist until after 512 transaction blocks
            length = 1500
        else:
            # the cost of looking up coins will be deflated because there are so
            # few, but a longer chain takes longer to make and test
            length = 500

    if length <= 0:
        print("the output blockchain must have at least length 1")
        sys.exit(1)

    if output is None:
        output = f"stress-test-blockchain-{length}-{fill_rate}{'-refs' if block_refs else ''}.sqlite"

    root_path = Path("./test-chain").resolve()
    root_path.mkdir(parents=True, exist_ok=True)
    with TempKeyring() as keychain:
        bt = create_block_tools(constants=test_constants, root_path=root_path, keychain=keychain)
        initialize_logging(
            "generate_chain", {"log_level": "DEBUG", "log_stdout": False, "log_syslog": False}, root_path=root_path
        )

        print(f"writing blockchain to {output}")
        with closing(sqlite3.connect(output)) as db:
            db.execute(
                "CREATE TABLE full_blocks("
                "header_hash blob PRIMARY KEY,"
                "prev_hash blob,"
                "height bigint,"
                "in_main_chain tinyint,"
                "block blob)"
            )

            wallet = bt.get_farmer_wallet_tool()
            farmer_puzzlehash = wallet.get_new_puzzlehash()
            pool_puzzlehash = wallet.get_new_puzzlehash()
            transaction_blocks: List[uint32] = []

            blocks = bt.get_consecutive_blocks(
                3,
                farmer_reward_puzzle_hash=farmer_puzzlehash,
                pool_reward_puzzle_hash=pool_puzzlehash,
                keep_going_until_tx_block=True,
                genesis_timestamp=uint64(1234567890),
                use_timestamp_residual=True,
            )

            unspent_coins: List[Coin] = []

            for b in blocks:
                for coin in b.get_included_reward_coins():
                    if coin.puzzle_hash in [farmer_puzzlehash, pool_puzzlehash]:
                        unspent_coins.append(coin)
                db.execute(
                    "INSERT INTO full_blocks VALUES(?, ?, ?, ?, ?)",
                    (
                        b.header_hash,
                        b.prev_header_hash,
                        b.height,
                        1,  # in_main_chain
                        zstd.compress(bytes(b)),
                    ),
                )
            db.commit()

            b = blocks[-1]

            num_tx_per_block = int(1010 * fill_rate / 100)

            while True:
                with enable_profiler(profile, b.height):
                    start_time = time.monotonic()

                    new_coins: List[Coin] = []
                    spend_bundles: List[SpendBundle] = []
                    i = 0
                    for i in range(num_tx_per_block):
                        if unspent_coins == []:
                            break
                        c = unspent_coins.pop(random.randrange(len(unspent_coins)))
                        receiver = wallet.get_new_puzzlehash()
                        bundle = wallet.generate_signed_transaction(uint64(c.amount // 2), receiver, c)
                        new_coins.extend(bundle.additions())
                        spend_bundles.append(bundle)

                    block_references: List[uint32]
                    if block_refs:
                        block_references = random.sample(transaction_blocks, min(len(transaction_blocks), 512))
                        random.shuffle(block_references)
                    else:
                        block_references = []

                    farmer_puzzlehash = wallet.get_new_puzzlehash()
                    pool_puzzlehash = wallet.get_new_puzzlehash()
                    prev_num_blocks = len(blocks)
                    blocks = bt.get_consecutive_blocks(
                        1,
                        blocks,
                        farmer_reward_puzzle_hash=farmer_puzzlehash,
                        pool_reward_puzzle_hash=pool_puzzlehash,
                        keep_going_until_tx_block=True,
                        transaction_data=SpendBundle.aggregate(spend_bundles),
                        previous_generator=block_references,
                        use_timestamp_residual=True,
                    )
                    prev_tx_block = b
                    prev_block = blocks[-2]
                    b = blocks[-1]
                    height = b.height
                    assert b.is_transaction_block()
                    transaction_blocks.append(height)

                    for bl in blocks[prev_num_blocks:]:
                        for coin in bl.get_included_reward_coins():
                            unspent_coins.append(coin)
                    unspent_coins.extend(new_coins)

                    if b.transactions_info:
                        actual_fill_rate = b.transactions_info.cost / test_constants.MAX_BLOCK_COST_CLVM
                        if b.transactions_info.cost > test_constants.MAX_BLOCK_COST_CLVM:
                            print(f"COST EXCEEDED: {b.transactions_info.cost}")
                    else:
                        actual_fill_rate = 0

                    end_time = time.monotonic()
                    if prev_tx_block is not None:
                        assert b.foliage_transaction_block
                        assert prev_tx_block.foliage_transaction_block
                        ts = b.foliage_transaction_block.timestamp - prev_tx_block.foliage_transaction_block.timestamp
                    else:
                        ts = 0

                    print(
                        f"height: {b.height} "
                        f"spends: {i+1} "
                        f"refs: {len(block_references)} "
                        f"fill_rate: {actual_fill_rate*100:.1f}% "
                        f"new coins: {len(new_coins)} "
                        f"unspent: {len(unspent_coins)} "
                        f"difficulty: {b.weight - prev_block.weight} "
                        f"timestamp: {ts} "
                        f"time: {end_time - start_time:0.2f}s "
                        f"tx-block-ratio: {len(transaction_blocks)*100/b.height:0.0f}% "
                    )

                    new_blocks = [
                        (
                            b.header_hash,
                            b.prev_header_hash,
                            b.height,
                            1,  # in_main_chain
                            zstd.compress(bytes(b)),
                        )
                        for b in blocks[prev_num_blocks:]
                    ]
                    db.executemany("INSERT INTO full_blocks VALUES(?, ?, ?, ?, ?)", new_blocks)
                    db.commit()
                    if height >= length:
                        break


if __name__ == "__main__":
    # pylint: disable = no-value-for-parameter
    main()
