from pathlib import Path
from typing import Optional, TextIO
from glob import glob
import re

output_file = Path(__file__).parent.resolve() / "python" / "chia_rs" / "chia_rs.pyi"
crates_dir = Path(__file__).parent.parent.resolve() / "crates"
input_dir = crates_dir / "chia-protocol" / "src"

ignore_structs = ["PyPlotParam"]

# enums are exposed to python as int
enums = set(
    ["NodeType", "ProtocolMessageTypes", "RejectStateReason", "MempoolRemoveReason"]
)


def transform_type(m: str) -> str:
    n, t = m.split(":")
    if "list[" in t:
        t = t.replace("list[", "Sequence[")
    elif "bytes32" == t.strip():
        t = " bytes"
    elif t.strip() in enums:
        t = " int"
    return f"{n}:{t}"


def print_class(
    file: TextIO,
    name: str,
    members: list[str],
    extra: Optional[list[str]] = None,
    martial_for_json_hint: Optional[str] = None,
    unmartial_from_json_hint: Optional[str] = None,
):
    def add_indent(x: str):
        return "\n    " + x

    if martial_for_json_hint is None:
        martial_for_json_hint = "dict[str, Any]"

    if unmartial_from_json_hint is None:
        unmartial_from_json_hint = martial_for_json_hint

    init_args = "".join([(",\n        " + transform_type(x)) for x in members])

    all_replace_parameters = []
    for m in members:
        replace_param_name, replace_type = m.split(":")
        if replace_param_name.startswith("a") and replace_param_name[1:].isnumeric():
            continue
        all_replace_parameters.append(
            f"{replace_param_name}: Union[{replace_type}, _Unspec] = _Unspec()"
        )

    if extra is not None:
        members.extend(extra)

    # TODO: could theoretically be detected from the use of #[streamable(subclass)]
    inheritable = name in ["SpendBundle", "Program"]

    # TODO: is __richcmp__ ever actually present?
    # def __richcmp__(self) -> Any: ...
    file.write(
        f"""
{"@disjoint_base" if inheritable else "@final"}
class {name}:{"".join(map(add_indent, members))}
    def __new__(
        cls{init_args}
    ) -> {name}: ...
    def __hash__(self) -> int: ...
    def __repr__(self) -> str: ...
    def __deepcopy__(self, memo: object) -> Self: ...
    def __copy__(self) -> Self: ...
    @classmethod
    def from_bytes(cls, blob: bytes) -> Self: ...
    @classmethod
    def from_bytes_unchecked(cls, blob: bytes) -> Self: ...
    @classmethod
    def parse_rust(cls, blob: ReadableBuffer, trusted: bool = False) -> tuple[Self, int]: ...
    def to_bytes(self) -> bytes: ...
    def __bytes__(self) -> bytes: ...
    def stream_to_bytes(self) -> bytes: ...
    def get_hash(self) -> bytes32: ...
    def to_json_dict(self) -> {martial_for_json_hint}: ...
    @classmethod
    def from_json_dict(cls, json_dict: {unmartial_from_json_hint}) -> Self: ...
"""
    )

    if len(all_replace_parameters) > 0:
        indent = ",\n        "
        file.write(
            f"""    def replace(self, *, {indent.join(all_replace_parameters)}) -> {name}: ...
"""
        )


primitives = {
    "Bytes32": "bytes32",
    "Bytes100": "bytes100",
    "Bytes": "bytes",
    "String": "str",
    "u8": "uint8",
    "u16": "uint16",
    "u32": "uint32",
    "u64": "uint64",
    "u128": "uint128",
    "i8": "int8",
    "i16": "int16",
    "i32": "int32",
    "i64": "int64",
    "i128": "int128",
    "bool": "bool",
}


def rust_type_to_python(t: str) -> str:
    t = t.strip()

    if t in enums:
        return "int"

    r = primitives.get(t)
    if r is not None:
        return r

    m = re.fullmatch("Vec<(.+)>", t)
    if m is not None:
        return f"list[{rust_type_to_python(m.group(1))}]"

    m = re.fullmatch("Option<(.+)>", t)
    if m is not None:
        return f"Optional[{rust_type_to_python(m.group(1))}]"

    m = re.fullmatch("\\((.+)\\)", t)
    if m is not None:
        inner_list = m.group(1).split(",")
        inner = ", ".join(map(lambda se: rust_type_to_python(se), inner_list))
        return f"tuple[{inner}]"

    m = re.fullmatch("\\[(.+); [0-9]+\\]", t)
    if m is not None:
        return f"list[{rust_type_to_python(m.group(1))}]"

    return t


def parse_rust_source(filename: str, upper_case: bool) -> list[tuple[str, list[str]]]:
    ret: list[tuple[str, list[str]]] = []
    in_struct: Optional[str] = None
    members: list[str] = []
    with open(filename) as f:
        for line in f:
            if not in_struct:
                if line.startswith("pub struct ") and "{" in line:
                    in_struct = line.split("pub struct ")[1].split("{")[0].strip()
                elif line.startswith("streamable_struct!") and "{" in line:
                    in_struct, line = line.split("(")[1].split("{")
                    in_struct = in_struct.strip()
                elif line.startswith("message_struct!") and "{" in line:
                    in_struct, line = line.split("(")[1].split("{")
                    in_struct = in_struct.strip()
                elif line.startswith("pub struct ") and "(" in line and ");" in line:
                    name = line.split("pub struct ")[1].split("(")[0].strip()
                    rust_args = line.split("(")[1].split(");")[0]
                    args = []
                    for idx, rust_type in enumerate(rust_args.split(",")):
                        py_type = rust_type_to_python(rust_type)
                        args.append(f"a{idx}: {py_type}")
                    ret.append((name, args))
                    continue
                else:
                    continue

            # we're parsing members
            # ignore macros
            if line.strip().startswith("#"):
                continue

            # a field
            if ":" in line and "///" not in line:
                name, rust_type = line.split("//")[0].strip().split(":")
                # members are separated by , in rust. Strip that off
                try:
                    rust_type, line = rust_type.rsplit(",", 1)
                except:
                    rust_type, line = rust_type.rsplit("}", 1)
                    line = "}" + line
                py_type = rust_type_to_python(rust_type)
                members.append(f"{name.upper() if upper_case else name}: {py_type}")

            # did we reach the end?
            if "}" in line:
                if in_struct not in ignore_structs:
                    ret.append((in_struct, members))
                members = []
                in_struct = None
                continue

    assert in_struct is None
    return ret


extra_members = {
    "Coin": [
        "def name(self) -> bytes32: ...",
    ],
    "ClassgroupElement": [
        "@staticmethod\n    def create(bytes) -> ClassgroupElement: ...",
        "@staticmethod\n    def get_default_element() -> ClassgroupElement: ...",
        "@staticmethod\n    def get_size() -> int: ...",
    ],
    "UnfinishedBlock": [
        "prev_header_hash: bytes32",
        "partial_hash: bytes32",
        "def is_transaction_block(self) -> bool: ...",
        "total_iters: uint128",
    ],
    "FullBlock": [
        "prev_header_hash: bytes32",
        "header_hash: bytes32",
        "def is_transaction_block(self) -> bool: ...",
        "total_iters: uint128",
        "height: uint32",
        "weight: uint128",
        "def get_included_reward_coins(self) -> list[Coin]: ...",
        "def is_fully_compactified(self) -> bool: ...",
    ],
    "HeaderBlock": [
        "prev_header_hash: bytes32",
        "prev_hash: bytes32",
        "height: uint32",
        "weight: uint128",
        "header_hash: bytes32",
        "total_iters: uint128",
        "log_string: str",
        "is_transaction_block: bool",
        "first_in_sub_slot: bool",
    ],
    "UnfinishedHeaderBlock": [
        "prev_header_hash: bytes32",
        "header_hash: bytes32",
        "total_iters: uint128",
    ],
    "RewardChainBlock": [
        "def get_unfinished(self) -> RewardChainBlockUnfinished: ...",
    ],
    "SubSlotData": [
        "def is_end_of_slot(self) -> bool: ...",
        "def is_challenge(self) -> bool: ...",
    ],
    "Program": [
        "def get_tree_hash(self) -> bytes32: ...",
        "@staticmethod\n    def default() -> Program: ...",
        "@staticmethod\n    def fromhex(h: str) -> Program: ...",
        "@staticmethod\n    def to(o: object) -> Program: ...",
        "def run_rust(self, max_cost: int, flags: int, args: object) -> tuple[int, LazyNode]: ...",
        "def uncurry_rust(self) -> tuple[LazyNode, LazyNode]: ...",
    ],
    "SpendBundle": [
        "@classmethod\n    def aggregate(cls, spend_bundles: list[SpendBundle]) -> Self: ...",
        "def name(self) -> bytes32: ...",
        "def removals(self) -> list[Coin]: ...",
        "def additions(self) -> list[Coin]: ...",
    ],
    "BlockRecord": [
        "is_transaction_block: bool",
        "first_in_sub_slot: bool",
        "def is_challenge_block(self, constants: ConsensusConstants) -> bool: ...",
        "def ip_sub_slot_total_iters(self, constants: ConsensusConstants) -> uint128: ...",
        "def sp_iters(self, constants: ConsensusConstants) -> uint64: ...",
        "def ip_iters(self, constants: ConsensusConstants) -> uint64: ...",
        "def sp_sub_slot_total_iters(self, constants: ConsensusConstants) -> uint128: ...",
        "def sp_total_iters(self, constants: ConsensusConstants) -> uint128: ...",
    ],
    "ProofOfSpace": [
        "def param(self) -> PlotParam: ...",
    ],
    "CoinRecord": [
        "@property\n    def spent(self) -> bool: ...",
        "@property\n    def name(self) -> bytes32: ...",
        "@property\n    def coin_state(self) -> CoinState: ...",
    ],
}

classes = []
for filepath in sorted(glob(str(input_dir / "*.rs"))):
    if filepath.endswith("bytes.rs") or filepath.endswith("lazy_node.rs"):
        continue
    classes.extend(parse_rust_source(filepath, upper_case=False))

classes.extend(
    parse_rust_source(
        str(crates_dir / "chia-consensus" / "src" / "consensus_constants.rs"),
        upper_case=True,
    )
)

with open(output_file, "w") as file:
    file.write(
        """
#
# this file is generated by generate_type_stubs.py
#

from typing import Optional, Sequence, Union, Any, ClassVar, final
from .sized_bytes import bytes32, bytes100
from .sized_ints import uint8, uint16, uint32, uint64, uint128, int8, int16, int32, int64
from typing_extensions import Self, disjoint_base

ReadableBuffer = Union[bytes, bytearray, memoryview]

class _Unspec:
    pass

def solution_generator(spends: Sequence[tuple[Coin, bytes, bytes]]) -> bytes: ...
def solution_generator_backrefs(spends: Sequence[tuple[Coin, bytes, bytes]]) -> bytes: ...

def is_canonical_serialization(buf: bytes) -> bool: ...

def compute_merkle_set_root(values: Sequence[bytes]) -> bytes: ...

def supports_fast_forward(spend: CoinSpend) -> bool : ...
def fast_forward_singleton(spend: CoinSpend, new_coin: Coin, new_parent: Coin) -> bytes: ...

def run_block_generator(
    program: ReadableBuffer, block_refs: list[ReadableBuffer], max_cost: int, flags: int, signature: G2Element, bls_cache: Optional[BLSCache], constants: ConsensusConstants
) -> tuple[Optional[int], Optional[SpendBundleConditions]]: ...

def run_block_generator2(
    program: ReadableBuffer, block_refs: list[ReadableBuffer], max_cost: int, flags: int, signature: G2Element, bls_cache: Optional[BLSCache], constants: ConsensusConstants
) -> tuple[Optional[int], Optional[SpendBundleConditions]]: ...

def additions_and_removals(
    program: ReadableBuffer, block_refs: list[ReadableBuffer], flags: int, constants: ConsensusConstants
) -> tuple[list[tuple[Coin, Optional[bytes]]], list[tuple[bytes32, Coin]]]: ...

def check_time_locks(
    removal_coin_records: dict[bytes32, CoinRecord],
    bundle_conds: SpendBundleConditions,
    prev_transaction_block_height: uint32,
    timestamp: uint64,
) -> Optional[int]: ...

def confirm_included_already_hashed(
    root: bytes32,
    item: bytes32,
    proof: bytes,
) -> bool: ...

def confirm_not_included_already_hashed(
    root: bytes32,
    item: bytes32,
    proof: bytes,
) -> bool: ...

def validate_clvm_and_signature(
    new_spend: SpendBundle,
    max_cost: int,
    constants: ConsensusConstants,
    flags: int,
) -> tuple[SpendBundleConditions, list[tuple[bytes32, GTElement]], float]: ...

def get_conditions_from_spendbundle(
    spend_bundle: SpendBundle,
    max_cost: int,
    constants: ConsensusConstants,
    height: int,
) -> SpendBundleConditions: ...

def get_spends_for_trusted_block(
    constants: ConsensusConstants,
    generator: Program,
    block_refs: list[ReadableBuffer],
    flags: int,
) -> dict[str, Any]: ...

def get_spends_for_trusted_block_with_conditions(
    constants: ConsensusConstants,
    generator: Program,
    block_refs: list[ReadableBuffer],
    flags: int,
) -> list[dict[str, Any]]: ...

def get_flags_for_height_and_constants(
    height: int,
    constants: ConsensusConstants
) -> int: ...

def calculate_ip_iters(
    constants: ConsensusConstants,
    sub_slot_iters: uint64,
    signage_point_index: uint8,
    required_iters: uint64,
) -> uint64: ...

def calculate_sp_iters(
    constants: ConsensusConstants,
    sub_slot_iters: uint64,
    signage_point_index: uint8,
) -> uint64: ...

def calculate_sp_interval_iters(
    constants: ConsensusConstants,
    sub_slot_iters: uint64,
) -> uint64: ...

def is_overflow_block(
    constants: ConsensusConstants,
    signage_point_index: uint8,
) -> bool: ...

def expected_plot_size(
    k: int
) -> int: ...


NO_UNKNOWN_CONDS: int = ...
STRICT_ARGS_COUNT: int = ...
LIMIT_HEAP: int = ...
ENABLE_KECCAK_OPS_OUTSIDE_GUARD: int = ...
MEMPOOL_MODE: int = ...
DONT_VALIDATE_SIGNATURE: int = ...
COMPUTE_FINGERPRINT: int = ...
COST_CONDITIONS: int = ...
SIMPLE_GENERATOR: int = ...

ELIGIBLE_FOR_DEDUP: int = ...
ELIGIBLE_FOR_FF: int = ...

NO_UNKNOWN_OPS: int = ...

def run_chia_program(
    program: bytes, args: bytes, max_cost: int, flags: int
) -> tuple[int, LazyNode]: ...

@final
class LazyNode:
    pair: Optional[tuple[LazyNode, LazyNode]]
    atom: Optional[bytes]

def serialized_length(program: ReadableBuffer) -> int: ...
def serialized_length_trusted(program: ReadableBuffer) -> int: ...
def tree_hash(blob: ReadableBuffer) -> bytes32: ...
def get_puzzle_and_solution_for_coin(program: ReadableBuffer, args: ReadableBuffer, max_cost: int, find_parent: bytes32, find_amount: int, find_ph: bytes32, flags: int) -> tuple[bytes, bytes]: ...
def get_puzzle_and_solution_for_coin2(generator: Program, block_refs: list[ReadableBuffer], max_cost: int, find_coin: Coin, flags: int) -> tuple[Program, Program]: ...

@final
class BLSCache:
    def __new__(cls, cache_size: Optional[int] = None) -> BLSCache: ...
    def len(self) -> int: ...
    def aggregate_verify(self, pks: list[G1Element], msgs: list[bytes], sig: G2Element) -> bool: ...
    def items(self) -> list[tuple[bytes, GTElement]]: ...
    def update(self, other: Sequence[tuple[bytes, GTElement]]) -> None: ...
    def evict(self, pks: list[G1Element], msgs: list[bytes]) -> None: ...

@final
class AugSchemeMPL:
    @staticmethod
    def sign(pk: PrivateKey, msg: bytes, prepend_pk: Optional[G1Element] = None) -> G2Element: ...
    @staticmethod
    def aggregate(sigs: Sequence[G2Element]) -> G2Element: ...
    @staticmethod
    def verify(pk: G1Element, msg: bytes, sig: G2Element) -> bool: ...
    @staticmethod
    def aggregate_verify(pks: Sequence[G1Element], msgs: Sequence[bytes], sig: G2Element) -> bool: ...
    @staticmethod
    def key_gen(seed: bytes) -> PrivateKey: ...
    @staticmethod
    def g2_from_message(msg: bytes) -> G2Element: ...
    @staticmethod
    def derive_child_sk(sk: PrivateKey, index: int) -> PrivateKey: ...
    @staticmethod
    def derive_child_sk_unhardened(sk: PrivateKey, index: int) -> PrivateKey: ...
    @staticmethod
    def derive_child_pk_unhardened(pk: G1Element, index: int) -> G1Element: ...

@final
class BlockBuilder:
    def add_spend_bundles(self, bundles: Sequence[SpendBundle], cost: uint64, constants: ConsensusConstants) -> tuple[bool, bool]: ...
    def cost(self) -> uint64: ...
    def finalize(self, constants: ConsensusConstants) -> tuple[bytes, G2Element, uint64]: ...

@final
class MerkleSet:
    def get_root(self) -> bytes32: ...
    def is_included_already_hashed(self, included_leaf: bytes32) -> tuple[bool, bytes]: ...
    def __new__(
        cls,
        leafs: list[bytes32],
    ) -> MerkleSet: ...

@final
class PlotParam:
    @staticmethod
    def make_v1(s: int) -> PlotParam: ...
    @staticmethod
    def make_v2(s: int) -> PlotParam: ...

    size_v1: Optional[uint8]
    strength_v2: Optional[uint8]

# Proof-of-space 2
@final
class QualityProof:
   def serialize(self) -> bytes32: ...

@final
class Prover:
    def __new__(cls, plot_path: str) -> Prover: ...
    def get_qualities_for_challenge(self, challenge: bytes32, proof_fragment_filter: int) -> list[QualityProof]: ...
    def get_partial_proof(self, quality: QualityProof) -> tuple[PartialProof, int]: ...
    def size(self) -> int: ...
    def plot_id(self) -> bytes32: ...
    def get_strength(self) -> int: ...
    def get_filename(self) -> str: ...
    def get_memo(self) -> bytes: ...
    def to_bytes(self) -> bytes: ...
    @staticmethod
    def from_bytes(b: bytes) -> Prover: ...

def create_v2_plot(filename: str,
    k: int,
    strength: int,
    plot_id: bytes32,
    memo: bytes,
) -> None: ...

def validate_proof_v2(plot_id: bytes32, size: int, challenge: bytes32, required_plot_strength: int, proof_fragment_scan_filter: int, proof: bytes) -> Optional[bytes32]: ...

def solve_proof(fragments: PartialProof, plot_id: bytes32, strength: int, k: int) -> bytes: ...

"""
    )

    print_class(
        file,
        "G1Element",
        [],
        [
            "SIZE: ClassVar[int] = ...",
            "def get_fingerprint(self) -> int: ...",
            "def verify(self, signature: G2Element, msg: bytes) -> bool: ...",
            "def pair(self, other: G2Element) -> GTElement: ...",
            "@staticmethod",
            "def generator() -> G1Element: ...",
            "def __str__(self) -> str: ...",
            "def __add__(self, other: G1Element) -> G1Element: ...",
            "def __iadd__(self, other: G1Element) -> G1Element: ...",
            "def derive_unhardened(self, idx: int) -> G1Element: ...",
        ],
        martial_for_json_hint="str",
        unmartial_from_json_hint="Union[str, bytes]",
    )
    print_class(
        file,
        "G2Element",
        [],
        [
            "SIZE: ClassVar[int] = ...",
            "def pair(self, other: G1Element) -> GTElement: ...",
            "@staticmethod",
            "def generator() -> G2Element: ...",
            "def __str__(self) -> str: ...",
            "def __add__(self, other: G2Element) -> G2Element: ...",
            "def __iadd__(self, other: G2Element) -> G2Element: ...",
        ],
        martial_for_json_hint="str",
        unmartial_from_json_hint="Union[str, bytes]",
    )
    print_class(
        file,
        "GTElement",
        [],
        [
            "SIZE: ClassVar[int] = ...",
            "def __str__(self) -> str: ...",
            "def __mul__(self, rhs: GTElement) -> GTElement: ...",
            "def __imul__(self, rhs: GTElement) -> GTElement : ...",
        ],
        martial_for_json_hint="str",
    )
    print_class(
        file,
        "PrivateKey",
        [],
        [
            "PRIVATE_KEY_SIZE: ClassVar[int] = ...",
            "def sign(self, msg: bytes, final_pk: Optional[G1Element] = None) -> G2Element: ...",
            "def get_g1(self) -> G1Element: ...",
            "def __str__(self) -> str: ...",
            "def public_key(self) -> G1Element: ...",
            "def derive_hardened(self, idx: int) -> PrivateKey: ...",
            "def derive_unhardened(self, idx: int) -> PrivateKey: ...",
            "@staticmethod",
            "def from_seed(seed: bytes) -> PrivateKey: ...",
        ],
        martial_for_json_hint="str",
    )

    print_class(
        file,
        "SpendConditions",
        [
            "coin_id: bytes32",
            "parent_id: bytes32",
            "puzzle_hash: bytes32",
            "coin_amount: int",
            "height_relative: Optional[int]",
            "seconds_relative: Optional[int]",
            "before_height_relative: Optional[int]",
            "before_seconds_relative: Optional[int]",
            "birth_height: Optional[int]",
            "birth_seconds: Optional[int]",
            "create_coin: list[tuple[bytes32, int, Optional[bytes]]]",
            "agg_sig_me: list[tuple[G1Element, bytes]]",
            "agg_sig_parent: list[tuple[G1Element, bytes]]",
            "agg_sig_puzzle: list[tuple[G1Element, bytes]]",
            "agg_sig_amount: list[tuple[G1Element, bytes]]",
            "agg_sig_puzzle_amount: list[tuple[G1Element, bytes]]",
            "agg_sig_parent_amount: list[tuple[G1Element, bytes]]",
            "agg_sig_parent_puzzle: list[tuple[G1Element, bytes]]",
            "flags: int",
            "execution_cost: int",
            "condition_cost: int",
            "fingerprint: bytes",
        ],
    )

    print_class(
        file,
        "SpendBundleConditions",
        [
            "spends: list[SpendConditions]",
            "reserve_fee: int",
            "height_absolute: int",
            "seconds_absolute: int",
            "before_height_absolute: Optional[int]",
            "before_seconds_absolute: Optional[int]",
            "agg_sig_unsafe: list[tuple[G1Element, bytes]]",
            "cost: int",
            "removal_amount: int",
            "addition_amount: int",
            "validated_signature: bool",
            "execution_cost: int",
            "condition_cost: int",
            "num_atoms: int",
            "num_pairs: int",
            "heap_size: int",
        ],
    )

    for item in classes:
        # TODO: adjust the system to provide this control via more paths
        martial_for_json_hint = None
        if item[0] == "Program":
            martial_for_json_hint = "str"

        print_class(
            file,
            item[0],
            item[1],
            extra_members.get(item[0]),
            martial_for_json_hint=martial_for_json_hint,
        )
