use std::fmt::Write;

use color_eyre::{
    eyre::{Context, Report},
    owo_colors::OwoColorize,
};
use thiserror::Error;
use tracing_indicatif::span_ext::IndicatifSpanExt;

use near_primitives::types::BlockReference;

use super::FetchAbiError;
use crate::common::{CallResultExt, RpcQueryResponseExt};

#[derive(Debug, Clone, interactive_clap::InteractiveClap)]
#[interactive_clap(input_context = crate::GlobalContext)]
#[interactive_clap(output_context = ContractContext)]
pub struct Contract {
    #[interactive_clap(skip_default_input_arg)]
    /// What is the contract account ID?
    contract_account_id: crate::types::account_id::AccountId,
    #[interactive_clap(named_arg)]
    /// Select network
    network_config: crate::network_view_at_block::NetworkViewAtBlockArgs,
}

impl Contract {
    pub fn input_contract_account_id(
        context: &crate::GlobalContext,
    ) -> color_eyre::eyre::Result<Option<crate::types::account_id::AccountId>> {
        crate::common::input_non_signer_account_id_from_used_account_list(
            &context.config.credentials_home_dir,
            "What is the contract account ID?",
        )
    }
}

#[derive(Clone)]
pub struct ContractContext(crate::network_view_at_block::ArgsForViewContext);

impl ContractContext {
    pub fn from_previous_context(
        previous_context: crate::GlobalContext,
        scope: &<Contract as interactive_clap::ToInteractiveClapContextScope>::InteractiveClapContextScope,
    ) -> color_eyre::eyre::Result<Self> {
        let on_after_getting_block_reference_callback: crate::network_view_at_block::OnAfterGettingBlockReferenceCallback = std::sync::Arc::new({
            let account_id: near_primitives::types::AccountId = scope.contract_account_id.clone().into();

            move |network_config, block_reference| {
                tokio::runtime::Runtime::new()
                    .unwrap()
                    .block_on(display_inspect_contract(
                        &account_id,
                        network_config,
                        block_reference,
                    ))
            }
        });
        Ok(Self(crate::network_view_at_block::ArgsForViewContext {
            config: previous_context.config,
            on_after_getting_block_reference_callback,
            interacting_with_account_ids: vec![scope.contract_account_id.clone().into()],
        }))
    }
}

impl From<ContractContext> for crate::network_view_at_block::ArgsForViewContext {
    fn from(item: ContractContext) -> Self {
        item.0
    }
}

#[tracing::instrument(name = "Obtaining the contract code ...", skip_all)]
async fn get_contract_code(
    account_id: &near_primitives::types::AccountId,
    network_config: &crate::config::NetworkConfig,
    block_reference: &near_primitives::types::BlockReference,
) -> color_eyre::eyre::Result<near_jsonrpc_client::methods::query::RpcQueryResponse> {
    tracing::info!(target: "near_teach_me", "Obtaining the contract code ...");
    network_config
        .json_rpc_client()
        .call(near_jsonrpc_client::methods::query::RpcQueryRequest {
            block_reference: block_reference.clone(),
            request: near_primitives::views::QueryRequest::ViewCode {
                account_id: account_id.clone(),
            },
        })
        .await
        .wrap_err_with(|| {
            format!(
                "Failed to fetch query ViewCode for <{}> on network <{}>",
                &account_id, network_config.network_name
            )
        })
}

#[tracing::instrument(name = "Contract inspection ...", skip_all)]
async fn display_inspect_contract(
    account_id: &near_primitives::types::AccountId,
    network_config: &crate::config::NetworkConfig,
    block_reference: &near_primitives::types::BlockReference,
) -> crate::CliResult {
    tracing::info!(target: "near_teach_me", "Contract inspection ...");
    let json_rpc_client = network_config.json_rpc_client();
    let view_code_response = get_contract_code(account_id, network_config, block_reference).await?;
    let contract_code_view =
        if let near_jsonrpc_primitives::types::query::QueryResponseKind::ViewCode(result) =
            view_code_response.kind
        {
            result
        } else {
            return Err(color_eyre::Report::msg("Error call result".to_string()));
        };

    let account_view = get_account_view(
        &network_config.network_name,
        &json_rpc_client,
        block_reference,
        account_id,
    )
    .await?;

    let access_keys = get_access_keys(
        &network_config.network_name,
        &json_rpc_client,
        block_reference,
        account_id,
    )
    .await?;

    let mut table = prettytable::Table::new();
    table.set_format(*prettytable::format::consts::FORMAT_NO_COLSEP);

    table.add_row(prettytable::row![
        Fg->account_id,
        format!("At block #{}\n({})", view_code_response.block_height, view_code_response.block_hash)
    ]);

    let (contract_type, contract_status, checksum_hex, checksum_base58, storage_used) = match (
        &account_view.code_hash,
        &account_view.global_contract_account_id,
        &account_view.global_contract_hash,
    ) {
        (_, Some(global_contract_account_id), None) => (
            "Global Contract",
            format!("deployed by account ID <{global_contract_account_id}>"),
            "no data".to_string(),
            "no data".to_string(),
            format!("{}", bytesize::ByteSize(account_view.storage_usage)),
        ),
        (_, None, Some(global_contract_hash)) => (
            "Global Contract",
            "deployed by Hash".to_string(),
            hex::encode(global_contract_hash.as_ref()),
            bs58::encode(global_contract_hash).into_string(),
            format!("{}", bytesize::ByteSize(account_view.storage_usage)),
        ),
        (code_hash, None, None) => (
            "Local Contract",
            String::new(),
            hex::encode(code_hash.as_ref()),
            bs58::encode(code_hash).into_string(),
            format!(
                "{} ({} Wasm + {} data)",
                bytesize::ByteSize(account_view.storage_usage),
                bytesize::ByteSize(u64::try_from(contract_code_view.code.len())?),
                bytesize::ByteSize(
                    account_view
                        .storage_usage
                        .checked_sub(u64::try_from(contract_code_view.code.len())?)
                        .expect("Unexpected error")
                )
            ),
        ),
        (_code_hash, _global_account_id, _global_hash) => (
            "Contract",
            "Invalid account contract state. Please contact the developers."
                .red()
                .to_string(),
            String::new(),
            String::new(),
            format!("{}", bytesize::ByteSize(account_view.storage_usage)),
        ),
    };

    table.add_row(prettytable::row![
        Fy->contract_type,
        contract_status
    ]);

    table.add_row(prettytable::row![
        Fy->"SHA-256 checksum [hex]",
        checksum_hex
    ]);

    table.add_row(prettytable::row![
        Fy->"SHA-256 checksum [base58]",
        checksum_base58
    ]);

    table.add_row(prettytable::row![
        Fy->"Storage used",
        storage_used
    ]);

    let access_keys_summary = if access_keys.is_empty() {
        "Contract is locked (no access keys)".to_string()
    } else {
        let full_access_keys_count = access_keys
            .iter()
            .filter(|access_key| {
                matches!(
                    access_key.access_key.permission,
                    near_primitives::views::AccessKeyPermissionView::FullAccess
                )
            })
            .count();
        format!(
            "{} full access keys and {} function-call-only access keys",
            full_access_keys_count,
            access_keys.len() - full_access_keys_count
        )
    };
    table.add_row(prettytable::row![
        Fy->"Access keys",
        access_keys_summary
    ]);

    match get_contract_source_metadata(&json_rpc_client, block_reference, account_id).await {
        Ok(contract_source_metadata) => {
            table.add_row(prettytable::row![
                Fy->"Contract version",
                contract_source_metadata.version.unwrap_or_default()
            ]);
            table.add_row(prettytable::row![
                Fy->"Contract link",
                contract_source_metadata.link.unwrap_or_default()
            ]);
            table.add_row(prettytable::row![
                Fy->"Supported standards",
                contract_source_metadata.standards
                    .iter()
                    .fold(String::new(), |mut output, standard| {
                        let _ = writeln!(output, "{} ({})", standard.standard, standard.version);
                        output
                    })
            ]);
        }
        Err(err) => {
            table.add_row(prettytable::row![
                "",
                textwrap::fill(
                    &format!(
                        "{}: {}",
                        match &err {
                            FetchContractSourceMetadataError::ContractSourceMetadataNotSupported => "Info",
                            FetchContractSourceMetadataError::ContractSourceMetadataUnknownFormat(_) |
                            FetchContractSourceMetadataError::RpcError(_) => "Warning",
                        },
                        err
                    ),
                    80
                )
            ]);

            table.add_row(prettytable::row![
                Fy->"Contract version",
                "N/A"
            ]);
            table.add_row(prettytable::row![
                Fy->"Contract link",
                "N/A"
            ]);
            table.add_row(prettytable::row![
                Fy->"Supported standards",
                "N/A"
            ]);
        }
    }

    match super::get_contract_abi(&json_rpc_client, block_reference, account_id).await {
        Ok(abi_root) => {
            table.add_row(prettytable::row![
                Fy->"NEAR ABI version",
                abi_root.schema_version
            ]);
            table.printstd();

            println!(
                "\n {} (hint: you can download full JSON Schema using `download-abi` command)",
                "Functions:".yellow()
            );
            for function in abi_root.body.functions {
                let mut table_func = prettytable::Table::new();
                table_func.set_format(*prettytable::format::consts::FORMAT_CLEAN);
                table_func.add_empty_row();

                table_func.add_row(prettytable::row![format!(
                    "{} ({}) {}\n{}",
                    format!(
                        "fn {}({}) -> {}",
                        function.name.green(),
                        "...".yellow(),
                        "...".blue()
                    ),
                    match function.kind {
                        near_abi::AbiFunctionKind::Call =>
                            "read-write function - transcation required",
                        near_abi::AbiFunctionKind::View => "read-only function",
                    },
                    function
                        .modifiers
                        .iter()
                        .fold(String::new(), |mut output, modifier| {
                            let _ = write!(
                                output,
                                "{} ",
                                match modifier {
                                    near_abi::AbiFunctionModifier::Init => "init".red(),
                                    near_abi::AbiFunctionModifier::Payable => "payable".red(),
                                    near_abi::AbiFunctionModifier::Private => "private".red(),
                                }
                            );
                            output
                        }),
                    function.doc.unwrap_or_default()
                )]);
                table_func.printstd();

                let mut table_args = prettytable::Table::new();
                table_args.set_format(*prettytable::format::consts::FORMAT_CLEAN);
                table_args.get_format().padding(1, 0);

                table_args.add_row(prettytable::row![
                    "...".yellow(),
                    Fy->"Arguments (JSON Schema):",
                ]);
                table_args.add_row(prettytable::row![
                    "   ",
                    if function.params.is_empty() {
                        "No arguments needed".to_string()
                    } else {
                        serde_json::to_string_pretty(&function.params).unwrap_or_default()
                    }
                ]);
                table_args.add_row(prettytable::row![
                    "...".blue(),
                    Fb->"Return Value (JSON Schema):",
                ]);
                table_args.add_row(prettytable::row![
                    "   ",
                    if let Some(result) = function.result {
                        serde_json::to_string_pretty(&result).unwrap_or_default()
                    } else {
                        "No return value".to_string()
                    }
                ]);
                table_args.printstd();
            }
        }
        Err(err) => {
            table.add_row(prettytable::row![
                Fy->"NEAR ABI version",
                textwrap::fill(
                    &format!(
                        "{}: {}",
                        match &err {
                            FetchAbiError::AbiNotSupported => "Info",
                            FetchAbiError::AbiUnknownFormat(_) | FetchAbiError::RpcError(_) => "Warning",
                        },
                        err
                    ),
                    80
                )
            ]);
            table.printstd();
            println!(
                "\n {} (NEAR ABI is not available, so only function names are extracted)\n",
                "Functions:".yellow()
            );

            let parser = wasmparser::Parser::new(0);
            for payload in parser.parse_all(&contract_code_view.code) {
                if let wasmparser::Payload::ExportSection(export_section) =
                    payload.wrap_err_with(|| {
                        format!(
                            "Could not parse WebAssembly binary of the contract <{account_id}>."
                        )
                    })?
                {
                    for export in export_section {
                        let export = export
                            .wrap_err_with(|| format!("Could not parse WebAssembly export section of the contract <{account_id}>."))?;
                        if let wasmparser::ExternalKind::Func = export.kind {
                            println!(
                                " fn {}({}) -> {}\n",
                                export.name.green(),
                                "...".yellow(),
                                "...".blue()
                            );
                        }
                    }
                }
            }
        }
    }

    Ok(())
}

#[tracing::instrument(name = "Getting information about", skip_all)]
async fn get_account_view(
    network_name: &str,
    json_rpc_client: &near_jsonrpc_client::JsonRpcClient,
    block_reference: &BlockReference,
    account_id: &near_primitives::types::AccountId,
) -> color_eyre::eyre::Result<near_primitives::views::AccountView> {
    tracing::Span::current().pb_set_message(&format!("{account_id} ..."));
    tracing::info!(target: "near_teach_me", "Getting information about {account_id} ...");
    for _ in 0..5 {
        let account_view_response = json_rpc_client
            .call(near_jsonrpc_client::methods::query::RpcQueryRequest {
                block_reference: block_reference.clone(),
                request: near_primitives::views::QueryRequest::ViewAccount {
                    account_id: account_id.clone(),
                },
            })
            .await;

        if let Err(near_jsonrpc_client::errors::JsonRpcError::TransportError(_)) =
            &account_view_response
        {
            eprintln!("Transport error.\nPlease wait. The next try to send this query is happening right now ...");
            std::thread::sleep(std::time::Duration::from_millis(100))
        } else {
            return account_view_response
                .wrap_err_with(|| {
                    format!(
                        "Failed to fetch query ViewAccount for contract <{account_id}> on network <{network_name}>" 
                    )
                })?
                .account_view();
        }
    }
    color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!(format!(
        "Transport error. Failed to fetch query ViewAccount for contract <{account_id}> on network <{network_name}>"
    )))
}

#[tracing::instrument(name = "Getting a list of", skip_all)]
async fn get_access_keys(
    network_name: &str,
    json_rpc_client: &near_jsonrpc_client::JsonRpcClient,
    block_reference: &BlockReference,
    account_id: &near_primitives::types::AccountId,
) -> color_eyre::eyre::Result<Vec<near_primitives::views::AccessKeyInfoView>> {
    tracing::Span::current().pb_set_message(&format!("{account_id} access keys ..."));
    tracing::info!(target: "near_teach_me", "Getting a list of {account_id} access keys ...");
    for _ in 0..5 {
        let access_keys_response = json_rpc_client
            .call(near_jsonrpc_client::methods::query::RpcQueryRequest {
                block_reference: block_reference.clone(),
                request: near_primitives::views::QueryRequest::ViewAccessKeyList {
                    account_id: account_id.clone(),
                },
            })
            .await;

        if let Err(near_jsonrpc_client::errors::JsonRpcError::TransportError(_)) =
            &access_keys_response
        {
            eprintln!("Transport error.\nPlease wait. The next try to send this query is happening right now ...");
            std::thread::sleep(std::time::Duration::from_millis(100))
        } else {
            return Ok(access_keys_response
                .wrap_err_with(|| {
                    format!(
                        "Failed to fetch ViewAccessKeyList for contract <{account_id}> on network <{network_name}>"
                    )
                })?
                .access_key_list_view()?
                .keys);
        }
    }
    color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!(format!(
        "Transport error. Failed to fetch query ViewAccessKeyList for contract <{account_id}> on network <{network_name}>"
    )))
}

#[derive(Error, Debug)]
pub enum FetchContractSourceMetadataError {
    #[error("Contract Source Metadata (https://nomicon.io/Standards/SourceMetadata) is not supported by the contract, so there is no way to get detailed information.")]
    ContractSourceMetadataNotSupported,
    #[error("'contract_source_metadata' function call failed due to RPC error, so there is no way to get Contract Source Metadata. See more details about the error:\n\n{0}")]
    RpcError(
        near_jsonrpc_client::errors::JsonRpcError<
            near_jsonrpc_primitives::types::query::RpcQueryError,
        >,
    ),
    #[error("The contract source metadata format is unknown (https://nomicon.io/Standards/SourceMetadata), so there is no way to get detailed information. See more details about the error:\n\n{0}")]
    ContractSourceMetadataUnknownFormat(Report),
}

#[tracing::instrument(name = "Getting contract source metadata for account", skip_all)]
pub async fn get_contract_source_metadata(
    json_rpc_client: &near_jsonrpc_client::JsonRpcClient,
    block_reference: &BlockReference,
    account_id: &near_primitives::types::AccountId,
) -> Result<
    near_verify_rs::types::contract_source_metadata::ContractSourceMetadata,
    FetchContractSourceMetadataError,
> {
    tracing::Span::current().pb_set_message(&format!("{account_id} ..."));
    tracing::info!(target: "near_teach_me", "Getting contract source metadata for account {account_id} ...");
    tracing::info!(
        target: "near_teach_me",
        parent: &tracing::Span::none(),
            "I am making HTTP call to NEAR JSON RPC to call a read-only function `contract_source_metadata` on `{}` account, learn more https://docs.near.org/api/rpc/contracts#call-a-contract-function",
            account_id
    );

    let mut retries_left = (0..5).rev();
    loop {
        let rpc_query_request = near_jsonrpc_client::methods::query::RpcQueryRequest {
            block_reference: block_reference.clone(),
            request: near_primitives::views::QueryRequest::CallFunction {
                account_id: account_id.clone(),
                method_name: "contract_source_metadata".to_owned(),
                args: near_primitives::types::FunctionArgs::from(vec![]),
            },
        };

        crate::common::teach_me_request_payload(json_rpc_client, &rpc_query_request);

        let contract_source_metadata_response = json_rpc_client.call(rpc_query_request).await;

        match contract_source_metadata_response {
            Err(near_jsonrpc_client::errors::JsonRpcError::TransportError(err)) => {
                if let Some(retries_left) = retries_left.next() {
                    sleep_after_error(format!(
                        "(Previous attempt failed with error: `{}`. Will retry {} more times)",
                        err.to_string().red(),
                        retries_left
                    ));
                } else {
                    return Err(FetchContractSourceMetadataError::RpcError(
                        near_jsonrpc_client::errors::JsonRpcError::TransportError(err),
                    ));
                }
            }
            Err(near_jsonrpc_client::errors::JsonRpcError::ServerError(
                near_jsonrpc_client::errors::JsonRpcServerError::HandlerError(
                    near_jsonrpc_primitives::types::query::RpcQueryError::ContractExecutionError {
                        vm_error,
                        ..
                    },
                ),
            )) if vm_error.contains("MethodNotFound") => {
                return Err(FetchContractSourceMetadataError::ContractSourceMetadataNotSupported);
            }
            Err(err) => {
                return Err(FetchContractSourceMetadataError::RpcError(err));
            }
            Ok(contract_source_metadata_response) => {
                return contract_source_metadata_response
                    .call_result()
                    .inspect(|call_result| {
                        tracing::info!(
                            target: "near_teach_me",
                            parent: &tracing::Span::none(),
                            "JSON RPC Response:\n{}",
                            crate::common::indent_payload(&format!(
                                "{{\n  \"block_hash\": {}\n  \"block_height\": {}\n  \"logs\": {:?}\n  \"result\": {:?}\n}}",
                                contract_source_metadata_response.block_hash,
                                contract_source_metadata_response.block_height,
                                call_result.logs,
                                call_result.result
                            ))
                        );
                        tracing::info!(
                            target: "near_teach_me",
                            parent: &tracing::Span::none(),
                            "Decoding the \"result\" array of bytes as UTF-8 string (tip: you can use this Python snippet to do it: `\"\".join([chr(c) for c in result])`):\n{}",
                            crate::common::indent_payload(&format!("{}\n ",
                                &String::from_utf8(call_result.result.clone())
                                    .unwrap_or_else(|_| "<decoding failed - the result is not a UTF-8 string>".to_owned())
                            ))
                        );
                    })
                    .inspect_err(|err| {
                        tracing::info!(
                            target: "near_teach_me",
                            parent: &tracing::Span::none(),
                            "JSON RPC Response:\n{}",
                            crate::common::indent_payload(&err.to_string())
                        );
                    })
                    .map_err(FetchContractSourceMetadataError::ContractSourceMetadataUnknownFormat)?
                    .parse_result_from_json::<near_verify_rs::types::contract_source_metadata::ContractSourceMetadata>()
                    .wrap_err("Failed to parse contract source metadata")
                    .map_err(
                        FetchContractSourceMetadataError::ContractSourceMetadataUnknownFormat,
                    );
            }
        }
    }
}

#[tracing::instrument(name = "Waiting 3 seconds before sending a request via RPC", skip_all)]
fn sleep_after_error(additional_message_for_name: String) {
    tracing::Span::current().pb_set_message(&additional_message_for_name);
    tracing::info!(target: "near_teach_me", "Waiting 3 seconds before sending a request via RPC {additional_message_for_name}");
    std::thread::sleep(std::time::Duration::from_secs(3));
}
