import argparse
import json
import logging
import uuid
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Tuple

from sqlalchemy import func, or_, text
from sqlalchemy.dialects.postgresql import insert
from sqlalchemy.engine import Row
from sqlalchemy.exc import IntegrityError, NoResultFound
from sqlalchemy.orm import Session
from web3 import Web3

from . import data, db
from .models import (
    Blockchain,
    CallRequest,
    CallRequestType,
    MetatxRequester,
    RegisteredContract,
)

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class CallRequestNotFound(Exception):
    """
    Raised when call request with the given parameters is not found in the database.
    """


class InvalidAddressFormat(Exception):
    """
    Raised when address not pass web3checksum validation.
    """


class UnsupportedCallRequestType(Exception):
    """
    Raised when unsupported call request type specified.
    """


class UnsupportedBlockchain(Exception):
    """
    Raised when unsupported blockchain specified.
    """


class CallRequestMethodValueError(Exception):
    """
    Raised when method not acceptable for specified request type.
    """


class CallRequestRequiredParamsValueError(Exception):
    """
    Raised when required params not acceptable for specified request type.
    """


class ContractAlreadyRegistered(Exception):
    pass


class CallRequestAlreadyRegistered(Exception):
    """
    Raised when call request with same parameters registered.
    """


def parse_registered_contract_response(
    obj: Tuple[RegisteredContract, Blockchain]
) -> data.RegisteredContractResponse:
    return data.RegisteredContractResponse(
        id=obj[0].id,
        blockchain=obj[1].name,
        address=obj[0].address,
        metatx_requester_id=obj[0].metatx_requester_id,
        title=obj[0].title,
        description=obj[0].description,
        image_uri=obj[0].image_uri,
        created_at=obj[0].created_at,
        updated_at=obj[0].updated_at,
    )


def parse_call_request_response(
    obj: Tuple[CallRequest, RegisteredContract]
) -> data.CallRequestResponse:
    return data.CallRequestResponse(
        id=obj[0].id,
        contract_id=obj[0].registered_contract_id,
        contract_address=obj[1].address,
        metatx_requester_id=obj[0].metatx_requester_id,
        call_request_type=obj[0].call_request_type_name,
        caller=obj[0].caller,
        method=obj[0].method,
        request_id=str(obj[0].request_id),
        parameters=obj[0].parameters,
        tx_hash=obj[0].tx_hash,
        expires_at=obj[0].expires_at,
        live_at=obj[0].live_at,
        created_at=obj[0].created_at,
        updated_at=obj[0].updated_at,
    )


def validate_method_and_params(
    call_request_type: str, method: str, parameters: Dict[str, Any]
) -> str:
    """
    Validate the given method and parameters for the specified contract_type.
    """
    if call_request_type == "dropper-v0.2.0":
        if method != "claim":
            raise CallRequestMethodValueError(
                "Method must be 'claim' for dropper contract type"
            )
        required_params = {
            "dropId",
            "blockDeadline",
            "amount",
            "signer",
            "signature",
        }
        if set(parameters.keys()) != required_params:
            raise CallRequestRequiredParamsValueError(
                f"Parameters must have {required_params} keys for dropper contract type"
            )
        try:
            Web3.toChecksumAddress(parameters["signer"])
        except:
            raise InvalidAddressFormat("Parameter signer must be a valid address")

    elif call_request_type == "raw":
        if method != "":
            raise CallRequestMethodValueError(
                "Method must be empty string for raw contract type"
            )
        if set(parameters.keys()) != {"calldata"}:
            raise CallRequestRequiredParamsValueError(
                "Parameters must have only 'calldata' key for raw contract type"
            )

    else:
        raise UnsupportedCallRequestType(f"Unknown contract type {call_request_type}")

    return call_request_type


def register_contract(
    db_session: Session,
    blockchain_name: str,
    address: str,
    metatx_requester_id: uuid.UUID,
    title: Optional[str],
    description: Optional[str],
    image_uri: Optional[str],
) -> Tuple[RegisteredContract, Blockchain]:
    """
    Register a contract against the Engine instance
    """
    try:
        blockchain = (
            db_session.query(Blockchain)
            .filter(Blockchain.name == blockchain_name)
            .one_or_none()
        )
        if blockchain is None:
            raise UnsupportedBlockchain("Unsupported blockchain specified")

        metatx_requester_stmt = insert(MetatxRequester.__table__).values(
            id=metatx_requester_id
        )
        metatx_requester_stmt_do_nothing_stmt = (
            metatx_requester_stmt.on_conflict_do_nothing()
        )
        db_session.execute(metatx_requester_stmt_do_nothing_stmt)

        contract = RegisteredContract(
            blockchain_id=blockchain.id,
            address=Web3.toChecksumAddress(address),
            metatx_requester_id=metatx_requester_id,
            title=title,
            description=description,
            image_uri=image_uri,
        )
        db_session.add(contract)
        db_session.commit()
    except IntegrityError as err:
        db_session.rollback()
        raise ContractAlreadyRegistered()
    except Exception as err:
        db_session.rollback()
        logger.error(repr(err))
        raise

    return (contract, blockchain)


def update_registered_contract(
    db_session: Session,
    metatx_requester_id: uuid.UUID,
    contract_id: uuid.UUID,
    title: Optional[str] = None,
    description: Optional[str] = None,
    image_uri: Optional[str] = None,
    ignore_nulls: bool = True,
) -> Tuple[RegisteredContract, Blockchain]:
    """
    Update the registered contract with the given contract ID provided that the user with metatx_requester_id
    has access to it.
    """
    contract_with_blockchain = (
        db_session.query(RegisteredContract, Blockchain)
        .join(Blockchain, Blockchain.id == RegisteredContract.blockchain_id)
        .filter(
            RegisteredContract.id == contract_id,
            RegisteredContract.metatx_requester_id == metatx_requester_id,
        )
        .one()
    )
    registered_contract, blockchain = contract_with_blockchain

    if not (title is None and ignore_nulls):
        registered_contract.title = title
    if not (description is None and ignore_nulls):
        registered_contract.description = description
    if not (image_uri is None and ignore_nulls):
        registered_contract.image_uri = image_uri

    try:
        db_session.add(registered_contract)
        db_session.commit()
    except Exception as err:
        logger.error(
            f"update_registered_contract -- error storing update in database: {repr(err)}"
        )
        db_session.rollback()
        raise

    return (registered_contract, blockchain)


def get_registered_contract(
    db_session: Session,
    metatx_requester_id: uuid.UUID,
    contract_id: uuid.UUID,
) -> Tuple[RegisteredContract, Blockchain]:
    """
    Get registered contract by ID.
    """
    contract_with_blockchain = (
        db_session.query(RegisteredContract, Blockchain)
        .join(Blockchain, Blockchain.id == RegisteredContract.blockchain_id)
        .filter(RegisteredContract.metatx_requester_id == metatx_requester_id)
        .filter(RegisteredContract.id == contract_id)
        .one()
    )
    registered_contract, blockchain = contract_with_blockchain

    return (registered_contract, blockchain)


def lookup_registered_contracts(
    db_session: Session,
    metatx_requester_id: uuid.UUID,
    blockchain: Optional[str] = None,
    address: Optional[str] = None,
    limit: int = 10,
    offset: Optional[int] = None,
) -> List[Row[Tuple[RegisteredContract, Blockchain]]]:
    """
    Lookup a registered contract
    """
    query = (
        db_session.query(RegisteredContract, Blockchain)
        .join(Blockchain, Blockchain.id == RegisteredContract.blockchain_id)
        .filter(RegisteredContract.metatx_requester_id == metatx_requester_id)
    )

    if blockchain is not None:
        query = query.filter(Blockchain.name == blockchain)

    if address is not None:
        query = query.filter(
            RegisteredContract.address == Web3.toChecksumAddress(address)
        )

    if offset is not None:
        query = query.offset(offset)

    contracts_with_blockchain = query.limit(limit).all()

    return contracts_with_blockchain


def delete_registered_contract(
    db_session: Session,
    metatx_requester_id: uuid.UUID,
    registered_contract_id: uuid.UUID,
) -> Tuple[RegisteredContract, Blockchain]:
    """
    Delete a registered contract
    """
    try:
        contract_with_blockchain = (
            db_session.query(RegisteredContract, Blockchain)
            .join(Blockchain, Blockchain.id == RegisteredContract.blockchain_id)
            .filter(RegisteredContract.metatx_requester_id == metatx_requester_id)
            .filter(RegisteredContract.id == registered_contract_id)
            .one()
        )
        contract = contract_with_blockchain[0]

        db_session.delete(contract)
        db_session.commit()
    except Exception as err:
        db_session.rollback()
        logger.error(repr(err))
        raise

    registered_contract, blockchain = contract_with_blockchain

    return (registered_contract, blockchain)


def create_request_calls(
    db_session: Session,
    metatx_requester_id: uuid.UUID,
    registered_contract_id: Optional[uuid.UUID],
    contract_address: Optional[str],
    call_specs: List[data.CallSpecification],
    ttl_days: Optional[int] = None,
    live_at: Optional[int] = None,
) -> int:
    """
    Batch creates call requests for the given registered contract.
    """
    # TODO(zomglings): Do not pass raw ttl_days into SQL query - could be subject to SQL injection
    # For now, in the interest of speed, let us just be super cautious with ttl_days.
    # Check that the ttl_days is indeed an integer
    if registered_contract_id is None and contract_address is None:
        raise ValueError(
            "At least one of registered_contract_id or contract_address is required"
        )

    if ttl_days is not None:
        assert ttl_days == int(ttl_days), "ttl_days must be an integer"
        if ttl_days <= 0:
            raise ValueError("ttl_days must be positive")

    if live_at is not None:
        assert live_at == int(live_at)
        if live_at <= 0:
            raise ValueError("live_at must be positive")

    # Check that the moonstream_user_id matches a RegisteredContract with the given id or address
    query = db_session.query(RegisteredContract).filter(
        RegisteredContract.metatx_requester_id == metatx_requester_id
    )

    if registered_contract_id is not None:
        query = query.filter(RegisteredContract.id == registered_contract_id)

    if contract_address is not None:
        query = query.filter(
            RegisteredContract.address == Web3.toChecksumAddress(contract_address)
        )

    try:
        registered_contract = query.one()
    except NoResultFound:
        raise ValueError("Invalid registered_contract_id or metatx_requester_id")

    # Normalize the caller argument using Web3.toChecksumAddress
    for specification in call_specs:
        normalized_caller = Web3.toChecksumAddress(specification.caller)

        # Validate the method and parameters for the contract_type
        try:
            call_request_type = validate_method_and_params(
                call_request_type=specification.call_request_type,
                method=specification.method,
                parameters=specification.parameters,
            )
        except UnsupportedCallRequestType as err:
            raise UnsupportedCallRequestType(err)
        except CallRequestMethodValueError as err:
            raise CallRequestMethodValueError(err)
        except CallRequestRequiredParamsValueError as err:
            raise CallRequestRequiredParamsValueError(err)
        except InvalidAddressFormat as err:
            raise InvalidAddressFormat(err)
        except Exception as err:
            logger.error(
                f"Unhandled error occurred during methods and parameters validation, err: {err}"
            )
            raise Exception()

        expires_at = None
        if ttl_days is not None:
            expires_at = func.now() + timedelta(days=ttl_days)

        request = CallRequest(
            registered_contract_id=registered_contract.id,
            call_request_type_name=call_request_type,
            metatx_requester_id=metatx_requester_id,
            caller=normalized_caller,
            method=specification.method,
            request_id=specification.request_id,
            parameters=specification.parameters,
            expires_at=expires_at,
            live_at=datetime.fromtimestamp(live_at) if live_at is not None else None,
        )

        db_session.add(request)
    # Insert the new rows into the database in a single transaction
    try:
        db_session.commit()
    except IntegrityError as err:
        db_session.rollback()
        raise CallRequestAlreadyRegistered()
    except Exception as e:
        db_session.rollback()
        raise e

    return len(call_specs)


def get_call_request(
    db_session: Session,
    request_id: uuid.UUID,
) -> Tuple[CallRequest, RegisteredContract]:
    """
    Get call request by ID.
    """
    results = (
        db_session.query(CallRequest, RegisteredContract)
        .join(
            RegisteredContract,
            CallRequest.registered_contract_id == RegisteredContract.id,
        )
        .filter(CallRequest.id == request_id)
        .all()
    )
    if len(results) == 0:
        raise CallRequestNotFound("Call request with given ID not found")
    elif len(results) != 1:
        raise Exception(
            f"Incorrect number of results found for request_id {request_id}"
        )

    call_request, registered_contract = results[0]

    return (call_request, registered_contract)


def list_blockchains(
    db_session: Session,
) -> List[Blockchain]:
    blockchains = db_session.query(Blockchain).all()
    return blockchains


def list_call_request_types(
    db_session: Session,
) -> List[CallRequestType]:
    call_request_types = db_session.query(CallRequestType).all()
    return call_request_types


def list_call_requests(
    db_session: Session,
    contract_id: Optional[uuid.UUID],
    contract_address: Optional[str],
    caller: Optional[str],
    limit: int = 10,
    offset: Optional[int] = None,
    show_expired: bool = False,
    live_after: Optional[int] = None,
    metatx_requester_id: Optional[uuid.UUID] = None,
) -> List[Row[Tuple[CallRequest, RegisteredContract, CallRequestType]]]:
    """
    List call requests.

    Argument moonstream_user_id took from authorization workflow. And if it is specified
    then user has access to call_requests before live_at param.
    """
    if caller is None:
        raise ValueError("caller must be specified")

    if contract_id is None and contract_address is None:
        raise ValueError(
            "At least one of contract_id or contract_address must be specified"
        )

    # If show_expired is False, filter out expired requests using current time on database server
    query = (
        db_session.query(CallRequest, RegisteredContract)
        .join(
            RegisteredContract,
            CallRequest.registered_contract_id == RegisteredContract.id,
        )
        .filter(CallRequest.caller == Web3.toChecksumAddress(caller))
    )

    if contract_id is not None:
        query = query.filter(CallRequest.registered_contract_id == contract_id)

    if contract_address is not None:
        query = query.filter(
            RegisteredContract.address == Web3.toChecksumAddress(contract_address)
        )

    if not show_expired:
        query = query.filter(
            CallRequest.expires_at > func.now(),
        )

    # If user id not specified, do not show call_requests before live_at.
    # Otherwise check show_before_live_at argument from query parameter
    if metatx_requester_id is not None:
        query = query.filter(
            CallRequest.metatx_requester_id == metatx_requester_id,
        )
    else:
        query = query.filter(
            or_(CallRequest.live_at < func.now(), CallRequest.live_at == None)
        )

    if live_after is not None:
        assert live_after == int(live_after)
        if live_after <= 0:
            raise ValueError("live_after must be positive")
        query = query.filter(CallRequest.live_at >= datetime.fromtimestamp(live_after))

    if offset is not None:
        query = query.offset(offset)

    query = query.limit(limit)
    results = query.all()
    return results


# TODO(zomglings): What should the delete functionality for call requests look like?
# - Delete expired requests for a given caller?
# - Delete all requests for a given caller?
# - Delete all requests for a given contract?
# - Delete request by ID?
# Should we implement these all using a single delete method, or a different method for each
# use case?
# Will come back to this once API is live.


def delete_requests(
    db_session: Session,
    metatx_requester_id: uuid.UUID,
    request_ids: List[uuid.UUID] = [],
) -> int:
    """
    Delete a requests.
    """
    try:
        requests_to_delete_query = (
            db_session.query(CallRequest)
            .filter(CallRequest.metatx_requester_id == metatx_requester_id)
            .filter(CallRequest.id.in_(request_ids))
        )
        requests_to_delete_num: int = requests_to_delete_query.delete(
            synchronize_session=False
        )
        db_session.commit()
    except Exception as err:
        db_session.rollback()
        logger.error(repr(err))
        raise Exception("Failed to delete call requests")

    return requests_to_delete_num


def complete_call_request(
    db_session: Session,
    tx_hash: str,
    call_request_id: uuid.UUID,
    caller: str,
) -> CallRequest:
    results = (
        db_session.query(CallRequest, RegisteredContract)
        .join(
            RegisteredContract,
            CallRequest.registered_contract_id == RegisteredContract.id,
        )
        .filter(CallRequest.id == call_request_id)
        .filter(CallRequest.caller == caller)
        .all()
    )

    if len(results) == 0:
        raise CallRequestNotFound("Call request with given ID not found")
    elif len(results) != 1:
        raise Exception(
            f"Incorrect number of results found for request_id {call_request_id}"
        )
    call_request, registered_contract = results[0]

    call_request.tx_hash = tx_hash

    try:
        db_session.add(call_request)
        db_session.commit()
    except Exception as err:
        logger.error(
            f"complete_call_request -- error updating in database: {repr(err)}"
        )
        db_session.rollback()
        raise

    return (call_request, registered_contract)


def handle_register(args: argparse.Namespace) -> None:
    """
    Handles the register command.
    """
    try:
        with db.yield_db_session_ctx() as db_session:
            contract = register_contract(
                db_session=db_session,
                blockchain=args.blockchain,
                address=args.address,
                contract_type=args.contract_type,
                moonstream_user_id=args.user_id,
                title=args.title,
                description=args.description,
                image_uri=args.image_uri,
            )
    except Exception as err:
        logger.error(err)
        return
    print(contract.json())


def handle_list(args: argparse.Namespace) -> None:
    """
    Handles the list command.
    """
    try:
        with db.yield_db_session_ctx() as db_session:
            contracts = lookup_registered_contracts(
                db_session=db_session,
                moonstream_user_id=args.user_id,
                blockchain=args.blockchain,
                address=args.address,
                contract_type=args.contract_type,
                limit=args.limit,
                offset=args.offset,
            )
    except Exception as err:
        logger.error(err)
        return

    print(json.dumps([contract.dict() for contract in contracts]))


def handle_delete(args: argparse.Namespace) -> None:
    """
    Handles the delete command.
    """
    try:
        with db.yield_db_session_ctx() as db_session:
            deleted_contract = delete_registered_contract(
                db_session=db_session,
                registered_contract_id=args.id,
                moonstream_user_id=args.user_id,
            )
    except Exception as err:
        logger.error(err)
        return

    print(deleted_contract.json())


def handle_request_calls(args: argparse.Namespace) -> None:
    """
    Handles the request-calls command.

    Reads a file of JSON-formatted call specifications from `args.call_specs`,
    validates them, and adds them to the call_requests table in the Engine database.

    :param args: The arguments passed to the CLI command.
    """
    with args.call_specs as ifp:
        try:
            call_specs_raw = json.load(ifp)
        except Exception as e:
            logger.error(f"Failed to load call specs: {e}")
            return

        call_specs = [data.CallSpecification(**spec) for spec in call_specs_raw]

    try:
        with db.yield_db_session_ctx() as db_session:
            create_request_calls(
                db_session=db_session,
                moonstream_user_id=args.moonstream_user_id,
                registered_contract_id=args.registered_contract_id,
                call_specs=call_specs,
                ttl_days=args.ttl_days,
            )
    except Exception as e:
        logger.error(f"Failed to request calls: {e}")
        return


def handle_list_requests(args: argparse.Namespace) -> None:
    """
    Handles the requests command.

    :param args: The arguments passed to the CLI command.
    """
    try:
        with db.yield_db_session_ctx() as db_session:
            call_requests = list_call_requests(
                db_session=db_session,
                contract_id=args.registered_contract_id,
                caller=args.caller,
                limit=args.limit,
                offset=args.offset,
                show_expired=args.show_expired,
            )
    except Exception as e:
        logger.error(f"Failed to list call requests: {e}")
        return

    print(json.dumps([request.dict() for request in call_requests]))


def generate_cli() -> argparse.ArgumentParser:
    """
    Generates a CLI which can be used to manage registered contracts on an Engine instance.
    """
    parser = argparse.ArgumentParser(description="Manage registered contracts")
    parser.set_defaults(func=lambda _: parser.print_help())
    subparsers = parser.add_subparsers()

    register_usage = "Register a new contract"
    register_parser = subparsers.add_parser(
        "register", help=register_usage, description=register_usage
    )
    register_parser.add_argument(
        "-b",
        "--blockchain",
        type=str,
        required=True,
        help="The blockchain the contract is deployed on",
    )
    register_parser.add_argument(
        "-a",
        "--address",
        type=str,
        required=True,
        help="The address of the contract",
    )
    register_parser.add_argument(
        "-c",
        "--contract-type",
        required=True,
        help="The type of the contract",
    )
    register_parser.add_argument(
        "-u",
        "--user-id",
        type=uuid.UUID,
        required=True,
        help="The ID of the Moonstream user under whom to register the contract",
    )
    register_parser.add_argument(
        "-t",
        "--title",
        type=str,
        required=False,
        default=None,
        help="The title of the contract",
    )
    register_parser.add_argument(
        "-d",
        "--description",
        type=str,
        required=False,
        default=None,
        help="The description of the contract",
    )
    register_parser.add_argument(
        "-i",
        "--image-uri",
        type=str,
        required=False,
        default=None,
        help="The image URI of the contract",
    )
    register_parser.set_defaults(func=handle_register)

    list_contracts_usage = "List all contracts matching certain criteria"
    list_contracts_parser = subparsers.add_parser(
        "list", help=list_contracts_usage, description=list_contracts_usage
    )
    list_contracts_parser.add_argument(
        "-b",
        "--blockchain",
        type=str,
        required=False,
        default=None,
        help="The blockchain the contract is deployed on",
    )
    list_contracts_parser.add_argument(
        "-a",
        "--address",
        type=str,
        required=False,
        default=None,
        help="The address of the contract",
    )
    list_contracts_parser.add_argument(
        "-c",
        "--contract-type",
        required=False,
        default=None,
        help="The type of the contract",
    )
    list_contracts_parser.add_argument(
        "-u",
        "--user-id",
        type=uuid.UUID,
        required=True,
        help="The ID of the Moonstream user whose contracts to list",
    )
    list_contracts_parser.add_argument(
        "-N",
        "--limit",
        type=int,
        required=False,
        default=10,
        help="The number of contracts to return",
    )
    list_contracts_parser.add_argument(
        "-n",
        "--offset",
        type=int,
        required=False,
        default=0,
        help="The offset to start returning contracts from",
    )
    list_contracts_parser.set_defaults(func=handle_list)

    delete_usage = "Delete a registered contract from an Engine instance"
    delete_parser = subparsers.add_parser(
        "delete", help=delete_usage, description=delete_usage
    )
    delete_parser.add_argument(
        "--id",
        type=uuid.UUID,
        required=True,
        help="The ID of the contract to delete",
    )
    delete_parser.add_argument(
        "-u",
        "--user-id",
        type=uuid.UUID,
        required=True,
        help="The ID of the Moonstream user whose contract to delete",
    )
    delete_parser.set_defaults(func=handle_delete)

    request_calls_usage = "Create call requests for a registered contract"
    request_calls_parser = subparsers.add_parser(
        "request-calls", help=request_calls_usage, description=request_calls_usage
    )
    request_calls_parser.add_argument(
        "-i",
        "--registered-contract-id",
        type=uuid.UUID,
        required=True,
        help="The ID of the registered contract to create call requests for",
    )
    request_calls_parser.add_argument(
        "-u",
        "--moonstream-user-id",
        type=uuid.UUID,
        required=True,
        help="The ID of the Moonstream user who owns the contract",
    )
    request_calls_parser.add_argument(
        "-c",
        "--calls",
        type=argparse.FileType("r"),
        required=True,
        help="Path to the JSON file with call specifications",
    )
    request_calls_parser.add_argument(
        "-t",
        "--ttl-days",
        type=int,
        required=False,
        default=None,
        help="The number of days until the call requests expire",
    )
    request_calls_parser.set_defaults(func=handle_request_calls)

    list_requests_usage = "List requests for calls on a registered contract"
    list_requests_parser = subparsers.add_parser(
        "requests", help=list_requests_usage, description=list_requests_usage
    )
    list_requests_parser.add_argument(
        "-i",
        "--registered-contract-id",
        type=uuid.UUID,
        required=True,
        help="The ID of the registered contract to list call requests for",
    )
    list_requests_parser.add_argument(
        "-c",
        "--caller",
        type=Web3.toChecksumAddress,
        required=True,
        help="Caller's address",
    )
    list_requests_parser.add_argument(
        "-N",
        "--limit",
        type=int,
        required=False,
        default=10,
        help="The number of call requests to return",
    )
    list_requests_parser.add_argument(
        "-n",
        "--offset",
        type=int,
        required=False,
        default=0,
        help="The offset to start returning contracts from",
    )
    list_requests_parser.add_argument(
        "--show-expired",
        action="store_true",
        help="Set this flag to also show expired requests. Default behavior is to hide these.",
    )
    list_requests_parser.set_defaults(func=handle_list_requests)

    return parser


def main() -> None:
    parser = generate_cli()
    args = parser.parse_args()
    args.func(args)


if __name__ == "__main__":
    main()
