from __future__ import annotations

import functools
from collections.abc import Iterable
from dataclasses import dataclass, replace
from enum import IntEnum
from typing import Optional, TypeVar

from klvm.casts import int_to_bytes

from chik.types.blockchain_format.coin import Coin, coin_as_list
from chik.types.blockchain_format.program import Program
from chik.types.blockchain_format.sized_bytes import bytes32
from chik.types.coin_spend import CoinSpend, make_spend
from chik.util.hash import std_hash
from chik.util.ints import uint16, uint64
from chik.util.streamable import Streamable, streamable
from chik.wallet.cat_wallet.cat_utils import CAT_MOD, construct_cat_puzzle
from chik.wallet.conditions import AssertCoinAnnouncement
from chik.wallet.lineage_proof import LineageProof, LineageProofField
from chik.wallet.payment import Payment
from chik.wallet.puzzles.load_klvm import load_klvm_maybe_recompile
from chik.wallet.puzzles.singleton_top_layer_v1_1 import SINGLETON_LAUNCHER_HASH, SINGLETON_MOD_HASH
from chik.wallet.uncurried_puzzle import UncurriedPuzzle, uncurry_puzzle
from chik.wallet.util.curry_and_treehash import curry_and_treehash
from chik.wallet.vc_wallet.vc_drivers import (
    COVENANT_LAYER_HASH,
    EML_TP_COVENANT_ADAPTER_HASH,
    EXTIGENT_METADATA_LAYER_HASH,
    GUARANTEED_NIL_TP_HASH,
    P2_ANNOUNCED_DELEGATED_PUZZLE_HASH,
    create_did_tp,
    create_eml_covenant_morpher,
)

# Mods

CREDENTIAL_RESTRICTION: Program = load_klvm_maybe_recompile(
    "credential_restriction.clsp",
    package_or_requirement="chik.wallet.vc_wallet.cr_puzzles",
    include_standard_libraries=True,
)
CREDENTIAL_RESTRICTION_HASH: bytes32 = CREDENTIAL_RESTRICTION.get_tree_hash()
PROOF_FLAGS_CHECKER: Program = load_klvm_maybe_recompile(
    "flag_proofs_checker.clsp",
    package_or_requirement="chik.wallet.vc_wallet.cr_puzzles",
    include_standard_libraries=True,
)
PENDING_VC_ANNOUNCEMENT: Program = load_klvm_maybe_recompile(
    "conditions_w_fee_announce.clsp",
    package_or_requirement="chik.wallet.vc_wallet.cr_puzzles",
    include_standard_libraries=True,
)
CREDENTIAL_STRUCT: Program = Program.to(
    (
        (
            (
                SINGLETON_MOD_HASH,
                SINGLETON_LAUNCHER_HASH,
            ),
            (
                EXTIGENT_METADATA_LAYER_HASH,
                EML_TP_COVENANT_ADAPTER_HASH,
            ),
        ),
        (
            curry_and_treehash(
                Program.to((1, EXTIGENT_METADATA_LAYER_HASH)).get_tree_hash_precalc(EXTIGENT_METADATA_LAYER_HASH),
                Program.to(EXTIGENT_METADATA_LAYER_HASH).get_tree_hash(),
                Program.to(None).get_tree_hash(),
                GUARANTEED_NIL_TP_HASH,
                Program.to(GUARANTEED_NIL_TP_HASH).get_tree_hash(),
                P2_ANNOUNCED_DELEGATED_PUZZLE_HASH,
            ),
            (
                Program.to(
                    int_to_bytes(2) + Program.to((1, COVENANT_LAYER_HASH)).get_tree_hash_precalc(COVENANT_LAYER_HASH)
                ),
                Program.to(
                    (
                        [
                            4,
                            (1, create_eml_covenant_morpher(create_did_tp().get_tree_hash())),
                            [4, (1, create_did_tp()), 1],
                        ],
                        None,
                    )
                ).get_tree_hash(),
            ),
        ),
    ),
)
CREDENTIAL_STRUCT_HASH: bytes32 = CREDENTIAL_STRUCT.get_tree_hash()


# Basic drivers
def construct_cr_layer(
    authorized_providers: list[bytes32],
    proofs_checker: Program,
    inner_puzzle: Program,
) -> Program:
    first_curry: Program = CREDENTIAL_RESTRICTION.curry(
        CREDENTIAL_STRUCT,
        authorized_providers,
        proofs_checker,
    )
    return first_curry.curry(first_curry.get_tree_hash(), inner_puzzle)


def construct_cr_layer_hash(
    authorized_providers_hash: bytes32,
    proofs_checker_hash: bytes32,
    inner_puzzle_hash: bytes32,
) -> bytes32:
    first_curry_hash: bytes32 = curry_and_treehash(
        Program.to((1, CREDENTIAL_RESTRICTION_HASH)).get_tree_hash_precalc(CREDENTIAL_RESTRICTION_HASH),
        CREDENTIAL_STRUCT_HASH,
        authorized_providers_hash,
        proofs_checker_hash,
    )
    first_curry_hash_hash: bytes32 = Program.to(first_curry_hash).get_tree_hash()
    final_hash: bytes32 = curry_and_treehash(
        Program.to((1, first_curry_hash)).get_tree_hash_precalc(first_curry_hash),
        first_curry_hash_hash,
        inner_puzzle_hash,
    )
    return final_hash


def match_cr_layer(
    uncurried_puzzle: UncurriedPuzzle,
) -> Optional[tuple[list[bytes32], Program, Program]]:
    extra_uncurried_puzzle = uncurry_puzzle(uncurried_puzzle.mod)
    if extra_uncurried_puzzle.mod == CREDENTIAL_RESTRICTION:
        return (
            [bytes32(provider.as_atom()) for provider in extra_uncurried_puzzle.args.at("rf").as_iter()],
            extra_uncurried_puzzle.args.at("rrf"),
            uncurried_puzzle.args.at("rf"),
        )
    else:
        return None


def solve_cr_layer(
    proof_of_inclusions: Program,
    proof_checker_solution: Program,
    provider_id: bytes32,
    vc_launcher_id: bytes32,
    vc_inner_puzhash: bytes32,
    my_coin_id: bytes32,
    inner_solution: Program,
) -> Program:
    solution: Program = Program.to(
        [
            proof_of_inclusions,
            proof_checker_solution,
            provider_id,
            vc_launcher_id,
            vc_inner_puzhash,
            my_coin_id,
            inner_solution,
        ]
    )
    return solution


# For the "pending approval" state
def construct_pending_approval_state(puzzle_hash: bytes32, amount: uint64) -> Program:
    return PENDING_VC_ANNOUNCEMENT.curry(Program.to([[51, puzzle_hash, amount, [puzzle_hash]]]))


_T_CRCAT = TypeVar("_T_CRCAT", bound="CRCAT")


@dataclass(frozen=True)
class CRCAT:
    coin: Coin
    tail_hash: bytes32
    lineage_proof: LineageProof
    authorized_providers: list[bytes32]
    proofs_checker: Program
    inner_puzzle_hash: bytes32

    @classmethod
    def launch(
        cls: type[_T_CRCAT],
        # General CAT launching info
        origin_coin: Coin,
        payment: Payment,
        tail: Program,
        tail_solution: Program,
        # CR Layer params
        authorized_providers: list[bytes32],
        proofs_checker: Program,
        # Probably never need this but some tail might
        optional_lineage_proof: Optional[LineageProof] = None,
    ) -> tuple[Program, CoinSpend, CRCAT]:
        """
        Launch a new CR-CAT from XCK.

        Returns a delegated puzzle to run that creates the eve CAT, an eve coin spend of the CAT, and the expected class
        representation after all relevant coin spends have been confirmed on chain.
        """
        tail_hash: bytes32 = tail.get_tree_hash()

        new_cr_layer_hash: bytes32 = construct_cr_layer(
            authorized_providers,
            proofs_checker,
            payment.puzzle_hash,  # type: ignore
        ).get_tree_hash_precalc(payment.puzzle_hash)
        new_cat_puzhash = construct_cat_puzzle(CAT_MOD, tail_hash, new_cr_layer_hash).get_tree_hash_precalc(
            new_cr_layer_hash
        )

        eve_innerpuz: Program = Program.to(
            (
                1,
                [
                    [51, new_cr_layer_hash, payment.amount, payment.memos],
                    [51, None, -113, tail, tail_solution],
                    [60, None],
                    [1, payment.puzzle_hash, authorized_providers, proofs_checker],
                ],
            )
        )
        eve_cat_puzzle: Program = construct_cat_puzzle(
            CAT_MOD,
            tail_hash,
            eve_innerpuz,
        )
        eve_cat_puzzle_hash: bytes32 = eve_cat_puzzle.get_tree_hash()

        eve_coin: Coin = Coin(origin_coin.name(), eve_cat_puzzle_hash, payment.amount)
        dpuz: Program = Program.to(
            (
                1,
                [
                    [51, eve_cat_puzzle_hash, payment.amount],
                    [61, std_hash(eve_coin.name())],
                ],
            )
        )

        eve_proof: LineageProof = LineageProof(
            eve_coin.parent_coin_info,
            eve_innerpuz.get_tree_hash(),
            uint64(eve_coin.amount),
        )

        return (
            dpuz,
            make_spend(
                eve_coin,
                eve_cat_puzzle,
                Program.to(  # solve_cat
                    [
                        None,
                        optional_lineage_proof,
                        eve_coin.name(),
                        coin_as_list(eve_coin),
                        eve_proof.to_program(),
                        0,
                        0,
                    ]
                ),
            ),
            CRCAT(
                Coin(eve_coin.name(), new_cat_puzhash, payment.amount),
                tail_hash,
                eve_proof,
                authorized_providers,
                proofs_checker,
                payment.puzzle_hash,
            ),
        )

    def construct_puzzle(self, inner_puzzle: Program) -> Program:
        return construct_cat_puzzle(
            CAT_MOD,
            self.tail_hash,
            self.construct_cr_layer(inner_puzzle),
        )

    def construct_cr_layer(self, inner_puzzle: Program) -> Program:
        return construct_cr_layer(
            self.authorized_providers,
            self.proofs_checker,
            inner_puzzle,
        )

    @staticmethod
    def is_cr_cat(puzzle_reveal: UncurriedPuzzle) -> tuple[bool, str]:
        """
        This takes an (uncurried) puzzle reveal and returns a boolean for whether the puzzle is a CR-CAT and an error
        message for if the puzzle is a mismatch.
        """
        if puzzle_reveal.mod != CAT_MOD:
            return False, "top most layer is not a CAT"  # pragma: no cover
        layer_below_cat: UncurriedPuzzle = uncurry_puzzle(uncurry_puzzle(puzzle_reveal.args.at("rrf")).mod)
        if layer_below_cat.mod != CREDENTIAL_RESTRICTION:
            return False, "CAT is not credential restricted"  # pragma: no cover

        return True, ""

    @staticmethod
    def get_inner_puzzle(puzzle_reveal: UncurriedPuzzle) -> Program:  # pragma: no cover
        return uncurry_puzzle(puzzle_reveal.args.at("rrf")).args.at("rf")

    @staticmethod
    def get_inner_solution(solution: Program) -> Program:  # pragma: no cover
        return solution.at("f").at("rrrrrrf")

    @classmethod
    def get_current_from_coin_spend(cls: type[_T_CRCAT], spend: CoinSpend) -> CRCAT:  # pragma: no cover
        uncurried_puzzle: UncurriedPuzzle = uncurry_puzzle(spend.puzzle_reveal)
        first_uncurried_cr_layer: UncurriedPuzzle = uncurry_puzzle(uncurried_puzzle.args.at("rrf"))
        second_uncurried_cr_layer: UncurriedPuzzle = uncurry_puzzle(first_uncurried_cr_layer.mod)
        lineage_proof = LineageProof.from_program(
            spend.solution.to_program().at("rf"),
            [LineageProofField.PARENT_NAME, LineageProofField.INNER_PUZZLE_HASH, LineageProofField.AMOUNT],
        )
        return CRCAT(
            spend.coin,
            bytes32(uncurried_puzzle.args.at("rf").as_atom()),
            lineage_proof,
            [bytes32(ap.as_atom()) for ap in second_uncurried_cr_layer.args.at("rf").as_iter()],
            second_uncurried_cr_layer.args.at("rrf"),
            first_uncurried_cr_layer.args.at("rf").get_tree_hash(),
        )

    @classmethod
    def get_next_from_coin_spend(
        cls: type[_T_CRCAT],
        parent_spend: CoinSpend,
        conditions: Optional[Program] = None,  # For optimization purposes, the conditions may already have been run
    ) -> list[CRCAT]:
        """
        Given a coin spend, this will return the next CR-CATs that were created as an output of that spend.
        Inner puzzle output conditions may also be supplied as an optimization.

        This is the main method to use when syncing. It can also sync from a CAT spend that was not a CR-CAT so long
        as the spend output a remark condition that was (REMARK authorized_providers proofs_checker)
        """
        coin_name: bytes32 = parent_spend.coin.name()
        puzzle: Program = parent_spend.puzzle_reveal.to_program()
        solution: Program = parent_spend.solution.to_program()

        # Get info by uncurrying
        _, tail_hash_as_prog, potential_cr_layer = puzzle.uncurry()[1].as_iter()
        new_inner_puzzle_hash: Optional[bytes32] = None
        if potential_cr_layer.uncurry()[0].uncurry()[0] != CREDENTIAL_RESTRICTION:
            # If the previous spend is not a CR-CAT:
            # we look for a remark condition that tells us the authorized_providers and proofs_checker
            inner_solution: Program = solution.at("f")
            if conditions is None:
                conditions = potential_cr_layer.run(inner_solution)
            for condition in conditions.as_iter():
                if condition.at("f") == Program.to(1):
                    new_inner_puzzle_hash = bytes32(condition.at("rf").as_atom())
                    authorized_providers_as_prog: Program = condition.at("rrf")
                    proofs_checker: Program = condition.at("rrrf")
                    break
            else:
                raise ValueError(
                    "Previous spend was not a CR-CAT, nor did it properly remark the CR params"
                )  # pragma: no cover
            authorized_providers = [bytes32(p.as_atom()) for p in authorized_providers_as_prog.as_iter()]
            lineage_inner_puzhash: bytes32 = potential_cr_layer.get_tree_hash()
        else:
            # Otherwise the info we need will be in the puzzle reveal
            cr_first_curry, self_hash_and_innerpuz = potential_cr_layer.uncurry()
            _, authorized_providers_as_prog, proofs_checker = cr_first_curry.uncurry()[1].as_iter()
            _, inner_puzzle = self_hash_and_innerpuz.as_iter()
            inner_solution = solution.at("f").at("rrrrrrf")
            if conditions is None:
                conditions = inner_puzzle.run(inner_solution)
            inner_puzzle_hash: bytes32 = inner_puzzle.get_tree_hash()
            authorized_providers = [bytes32(p.as_atom()) for p in authorized_providers_as_prog.as_iter()]
            lineage_inner_puzhash = construct_cr_layer(
                authorized_providers,
                proofs_checker,
                inner_puzzle_hash,  # type: ignore
            ).get_tree_hash_precalc(inner_puzzle_hash)

        # Convert all of the old stuff into python
        new_lineage_proof: LineageProof = LineageProof(
            parent_spend.coin.parent_coin_info,
            lineage_inner_puzhash,
            uint64(parent_spend.coin.amount),
        )

        all_conditions: list[Program] = list(conditions.as_iter())
        if len(all_conditions) > 1000:
            raise RuntimeError("More than 1000 conditions not currently supported by CRCAT drivers")  # pragma: no cover

        # Almost complete except the coin's full puzzle hash which we want to use the class method to calculate
        partially_completed_crcats: list[CRCAT] = [
            CRCAT(
                Coin(coin_name, bytes(32), uint64(condition.at("rrf").as_int())),
                bytes32(tail_hash_as_prog.as_atom()),
                new_lineage_proof,
                authorized_providers,
                proofs_checker,
                bytes32(condition.at("rf").as_atom()) if new_inner_puzzle_hash is None else new_inner_puzzle_hash,
            )
            for condition in all_conditions
            if condition.at("f").as_int() == 51 and condition.at("rrf") != Program.to(-113)
        ]

        return [
            replace(
                crcat,
                coin=Coin(
                    crcat.coin.parent_coin_info,
                    crcat.construct_puzzle(crcat.inner_puzzle_hash).get_tree_hash_precalc(  # type: ignore
                        crcat.inner_puzzle_hash
                    ),
                    crcat.coin.amount,
                ),
            )
            for crcat in partially_completed_crcats
        ]

    def do_spend(
        self,
        # CAT solving info
        previous_coin_id: bytes32,
        next_coin_proof: LineageProof,
        previous_subtotal: int,
        extra_delta: int,
        # CR layer solving info
        proof_of_inclusions: Program,
        proof_checker_solution: Program,
        provider_id: bytes32,
        vc_launcher_id: bytes32,
        vc_inner_puzhash: Optional[bytes32],  # Optional for incomplete spends
        # Inner puzzle and solution
        inner_puzzle: Program,
        inner_solution: Program,
        # For optimization purposes the conditions may already have been run
        conditions: Optional[Iterable[Program]] = None,
    ) -> tuple[list[AssertCoinAnnouncement], CoinSpend, list[CRCAT]]:
        """
        Spend a CR-CAT.

        Must give the CAT accounting information, the valid VC proof, and the inner puzzle and solution.  The function
        will return the announcement IDs for the VC to optionally assert, the spend of this CAT, and the class
        representations of any CR-CAT outputs.

        Likely, spend_many is more useful.
        """
        # Gather the output information
        announcements: list[AssertCoinAnnouncement] = []
        new_inner_puzzle_hashes_and_amounts: list[tuple[bytes32, uint64]] = []
        if conditions is None:
            conditions = inner_puzzle.run(inner_solution).as_iter()  # pragma: no cover
        assert conditions is not None
        for condition in conditions:
            if condition.at("f").as_int() == 51 and condition.at("rrf").as_int() != -113:
                new_inner_puzzle_hash: bytes32 = bytes32(condition.at("rf").as_atom())
                new_amount: uint64 = uint64(condition.at("rrf").as_int())
                announcements.append(
                    AssertCoinAnnouncement(
                        asserted_id=self.coin.name(),
                        asserted_msg=b"\xcd" + std_hash(new_inner_puzzle_hash + int_to_bytes(new_amount)),
                    )
                )
                new_inner_puzzle_hashes_and_amounts.append((new_inner_puzzle_hash, new_amount))

        return (
            announcements,
            make_spend(
                self.coin,
                self.construct_puzzle(inner_puzzle),
                Program.to(  # solve_cat
                    [
                        solve_cr_layer(
                            proof_of_inclusions,
                            proof_checker_solution,
                            provider_id,
                            vc_launcher_id,
                            vc_inner_puzhash,  # type: ignore
                            self.coin.name(),
                            inner_solution,
                        ),
                        self.lineage_proof.to_program(),
                        previous_coin_id,
                        coin_as_list(self.coin),
                        next_coin_proof.to_program(),
                        previous_subtotal,
                        extra_delta,
                    ]
                ),
            ),
            [
                CRCAT(
                    Coin(
                        self.coin.name(),
                        self.construct_puzzle(new_inner_puzzle_hash).get_tree_hash_precalc(  # type: ignore
                            new_inner_puzzle_hash
                        ),
                        new_amount,
                    ),
                    self.tail_hash,
                    LineageProof(
                        self.coin.parent_coin_info,
                        self.construct_cr_layer(self.inner_puzzle_hash).get_tree_hash_precalc(  # type: ignore
                            self.inner_puzzle_hash
                        ),
                        uint64(self.coin.amount),
                    ),
                    self.authorized_providers,
                    self.proofs_checker,
                    new_inner_puzzle_hash,
                )
                for new_inner_puzzle_hash, new_amount in new_inner_puzzle_hashes_and_amounts
            ],
        )

    @classmethod
    def spend_many(
        cls: type[_T_CRCAT],
        inner_spends: list[tuple[_T_CRCAT, int, Program, Program]],  # CRCAT, extra_delta, inner puzzle, inner solution
        # CR layer solving info
        proof_of_inclusions: Program,
        proof_checker_solution: Program,
        provider_id: bytes32,
        vc_launcher_id: bytes32,
        vc_inner_puzhash: Optional[bytes32],  # Optional for incomplete spends
    ) -> tuple[list[AssertCoinAnnouncement], list[CoinSpend], list[CRCAT]]:
        """
        Spend a multiple CR-CATs.

        This class will handle all of the CAT accounting information, the only necessary information is the inner
        puzzle/solution, and the proof of a valid VC being spent along side all of the coins. There is currently no
        support for multiple VCs being used across the spend.  There is also currently no support for minting/melting.
        """

        def next_index(index: int) -> int:
            return 0 if index == len(inner_spends) - 1 else index + 1

        def prev_index(index: int) -> int:
            return index - 1

        sorted_inner_spends: list[tuple[_T_CRCAT, int, Program, Program]] = sorted(
            inner_spends,
            key=lambda spend: spend[0].coin.name(),
        )

        all_expected_announcements: list[AssertCoinAnnouncement] = []
        all_coin_spends: list[CoinSpend] = []
        all_new_crcats: list[CRCAT] = []

        subtotal: int = 0
        for i, inner_spend in enumerate(sorted_inner_spends):
            crcat, extra_delta, inner_puzzle, inner_solution = inner_spend
            conditions: list[Program] = list(inner_puzzle.run(inner_solution).as_iter())
            output_amount: int = (
                sum(
                    c.at("rrf").as_int()
                    for c in conditions
                    if c.at("f").as_int() == 51 and c.at("rrf").as_int() != -113
                )
                - extra_delta
            )
            next_crcat, _, _, _ = sorted_inner_spends[next_index(i)]
            prev_crcat, _, _, _ = sorted_inner_spends[prev_index(i)]
            expected_announcements, coin_spend, new_crcats = crcat.do_spend(
                prev_crcat.coin.name(),
                LineageProof(
                    next_crcat.coin.parent_coin_info,
                    next_crcat.construct_cr_layer(
                        next_crcat.inner_puzzle_hash,  # type: ignore
                    ).get_tree_hash_precalc(next_crcat.inner_puzzle_hash),
                    uint64(next_crcat.coin.amount),
                ),
                subtotal,
                extra_delta,
                proof_of_inclusions,
                proof_checker_solution,
                provider_id,
                vc_launcher_id,
                vc_inner_puzhash,
                inner_puzzle,
                inner_solution,
                conditions=conditions,
            )
            all_expected_announcements.extend(expected_announcements)
            all_coin_spends.append(coin_spend)
            all_new_crcats.extend(new_crcats)

            subtotal = subtotal + crcat.coin.amount - output_amount

        return all_expected_announcements, all_coin_spends, all_new_crcats

    def expected_announcement(self) -> bytes32:
        """
        The announcement a VC must make to this CAT in order to spend it
        """
        return std_hash(self.coin.name() + b"\xca")


@dataclass(frozen=True)
class CRCATSpend:
    crcat: CRCAT
    inner_puzzle: Program
    inner_solution: Program
    children: list[CRCAT]
    incomplete: bool
    inner_conditions: list[Program]
    proof_of_inclusions: Program

    @classmethod
    def from_coin_spend(cls, spend: CoinSpend) -> CRCATSpend:  # pragma: no cover
        inner_puzzle: Program = CRCAT.get_inner_puzzle(uncurry_puzzle(spend.puzzle_reveal))
        inner_solution: Program = CRCAT.get_inner_solution(spend.solution.to_program())
        inner_conditions: Program = inner_puzzle.run(inner_solution)
        return cls(
            CRCAT.get_current_from_coin_spend(spend),
            inner_puzzle,
            inner_solution,
            CRCAT.get_next_from_coin_spend(spend, conditions=inner_conditions),
            spend.solution.to_program().at("f").at("rrrrf") == Program.to(None),
            list(inner_conditions.as_iter()),
            spend.solution.to_program().at("f").at("f"),
        )


@streamable
@dataclass(frozen=True)
class ProofsChecker(Streamable):
    flags: list[str]

    def as_program(self) -> Program:
        def byte_sort_flags(f1: str, f2: str) -> int:
            return 1 if Program.to([10, (1, f1), (1, f2)]).run([]) == Program.to(None) else -1

        return PROOF_FLAGS_CHECKER.curry(
            [
                Program.to((flag, "1"))
                for flag in sorted(
                    self.flags,
                    key=functools.cmp_to_key(byte_sort_flags),
                )
            ]
        )

    @classmethod
    def from_program(cls, uncurried_puzzle: UncurriedPuzzle) -> ProofsChecker:
        if uncurried_puzzle.mod != PROOF_FLAGS_CHECKER:
            raise ValueError("Puzzle was not a proof checker")  # pragma: no cover

        return cls([flag.at("f").as_atom().decode("utf8") for flag in uncurried_puzzle.args.at("f").as_iter()])


class CRCATVersion(IntEnum):
    V1 = uint16(1)


@streamable
@dataclass(frozen=True)
class CRCATMetadata(Streamable):
    lineage_proof: LineageProof
    inner_puzzle_hash: bytes32
