# This example shows a simple custom pipeline that ingests and decodes erc20 transfers into duckdb
# Tiders is published to PyPI as tiders and tiders-core.
# To install it, run: pip install tiders tiders-core
# Or with uv: uv pip install tiders tiders-core

# You can run this script with:
# uv run examples/end_to_end/erc20_custom.py --provider hypersync

# After run, you can see the result in the database:
# duckdb data/transfers.db
# SELECT * FROM transfers LIMIT 3;

import pyarrow as pa
from tiders import config as cc
from tiders import run_pipeline
from tiders_core import ingest, evm_signature_to_topic0
import logging
import os
import asyncio
from pathlib import Path
from dotenv import load_dotenv
import traceback
from typing import Dict, Optional, Any
import argparse
import duckdb
import datafusion

load_dotenv()

logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO").upper())
logger = logging.getLogger(__name__)

# Create directories
DATA_PATH = str(Path.cwd() / "data")
Path(DATA_PATH).mkdir(parents=True, exist_ok=True)

db_path = "./data/transfers"


# Read the last block number we have written so far to the database
def get_start_block(con: duckdb.DuckDBPyConnection) -> int:
    try:
        res = con.sql("SELECT MAX(block_number) from transfers").fetchone()
        if res is not None:
            return int(res[0])
        else:
            return 0
    except Exception:
        logger.warning(f"failed to get start block from db: {traceback.format_exc()}")
        return 0


# processing step using datafusion
def join_data(
    session_ctx: datafusion.SessionContext,
    data: Dict[str, datafusion.DataFrame],
    _: Any,
) -> Dict[str, datafusion.DataFrame]:
    _ = data

    bn = session_ctx.sql(
        "SELECT MIN(number) as min_block, MAX(NUMBER) as max_block FROM blocks"
    ).to_pylist()[0]

    logger.info(f"processing data from: {bn['min_block']} to: {bn['max_block']}")

    out = session_ctx.sql("""
        SELECT transfers.*, blocks.timestamp as block_timestamp FROM transfers
        INNER JOIN blocks ON blocks.number = transfers.block_number
    """)

    return {"transfers": out}


async def print_last_processed_transfers(db: duckdb.DuckDBPyConnection):
    while True:
        await asyncio.sleep(5)

        data = db.sql(
            'SELECT block_number, transaction_hash, "from", "to", amount FROM transfers ORDER BY block_number, log_index DESC LIMIT 10'
        )
        logger.info("printing last 10 transfers:")
        logger.info(f"\n{data}")


async def main(provider_kind: ingest.ProviderKind, url: Optional[str]):
    # Start duckdb
    connection = duckdb.connect(database=db_path).cursor()

    from_block = get_start_block(connection.cursor())
    logger.info(f"starting to ingest from block {from_block}")

    provider = ingest.ProviderConfig(
        kind=provider_kind,
        url=url,
    )

    query = ingest.Query(
        kind=ingest.QueryKind.EVM,
        params=ingest.evm.Query(
            from_block=from_block,
            # Select the logs we are interested in
            logs=[
                ingest.evm.LogRequest(
                    # Don't pass address filter to get all erc20 transfers
                    # address=[
                    #     "0xdAC17F958D2ee523a2206206994597C13D831ec7",  # USDT
                    #     "0xB8c77482e45F1F44dE1745F52C74426C631bDD52",  # BNB
                    #     "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",  # USDC
                    #     "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84",  # stETH
                    #     "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",  # Wrapped BTC
                    #     "0x582d872A1B094FC48F5DE31D3B73F2D9bE47def1",  # Wrapped TON coin
                    # ],
                    topic0=[
                        evm_signature_to_topic0("Transfer(address,address,uint256)")
                    ],
                    # include the blocks related to our logs
                    include_blocks=True,
                )
            ],
            # select the fields we want
            fields=ingest.evm.Fields(
                block=ingest.evm.BlockFields(number=True, timestamp=True),
                log=ingest.evm.LogFields(
                    block_number=True,
                    transaction_hash=True,
                    log_index=True,
                    address=True,
                    topic0=True,
                    topic1=True,
                    topic2=True,
                    topic3=True,
                    data=True,
                ),
            ),
        ),
    )

    writer = cc.Writer(
        kind=cc.WriterKind.DUCKDB,
        config=cc.DuckdbWriterConfig(
            connection=connection.cursor(),
        ),
    )

    pipeline = cc.Pipeline(
        provider=provider,
        writer=writer,
        query=query,
        steps=[
            cc.Step(
                kind=cc.StepKind.EVM_DECODE_EVENTS,
                config=cc.EvmDecodeEventsConfig(
                    event_signature="Transfer(address indexed from, address indexed to, uint256 amount)",
                    output_table="transfers",
                    # Write null if decoding fails instead of erroring out.
                    #
                    # This is needed if we are trying to decode all logs that match our topic0 without
                    # filtering for contract address, because other events like NFT transfers also match our topic0
                    allow_decode_fail=True,
                ),
            ),
            cc.Step(
                kind=cc.StepKind.DATAFUSION,
                config=cc.DataFusionStepConfig(
                    runner=join_data,
                ),
            ),
            # hex encode all binary fields to it is easy to view them in db
            # this is not good for performance but nice for easily seeing the values when doing queries to the database
            cc.Step(
                kind=cc.StepKind.HEX_ENCODE,
                config=cc.HexEncodeConfig(),
            ),
            # Have to do this because duckdb doesn't support 256 bit decimals :)
            cc.Step(
                name="i256_to_i128",
                kind=cc.StepKind.CAST_BY_TYPE,
                config=cc.CastByTypeConfig(
                    from_type=pa.decimal256(76, 0),
                    to_type=pa.decimal128(38, 0),
                    allow_cast_fail=True,
                ),
            ),
        ],
    )

    asyncio.create_task(print_last_processed_transfers(connection.cursor()))

    await run_pipeline(pipeline=pipeline)


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="example")

    parser.add_argument(
        "--provider",
        choices=["sqd", "hypersync"],
        required=True,
        help="Specify the provider ('sqd' or 'hypersync')",
    )

    args = parser.parse_args()

    url = None

    if args.provider == ingest.ProviderKind.HYPERSYNC:
        url = "https://eth.hypersync.xyz"
    elif args.provider == ingest.ProviderKind.SQD:
        url = "https://portal.sqd.dev/datasets/ethereum-mainnet"

    asyncio.run(main(args.provider, url))
