use std::str::FromStr;

use serde::Serialize;
use serde_json::{Map, Number, Value};

use crate::address;
use crate::crypto;
use crate::error::BrowserCoreError;
use crate::{PRIVATE_KEY_LENGTH, SIGNATURE_LENGTH};

#[derive(Clone, Copy)]
enum FieldKind {
    U8,
    U16,
    U32,
    U64,
    U128,
    I8,
    I16,
    I32,
    I64,
    I128,
    Bool,
    Address,
    VanityAddress,
    Hash(usize),
    Text(usize),
    PaddedText(usize),
}

#[derive(Clone, Copy)]
struct Field {
    name: &'static str,
    kind: FieldKind,
}

#[derive(Clone, Copy, PartialEq, Eq)]
enum SignatureMode {
    Single,
    Dual,
}

struct Schema {
    txtype: u8,
    name: &'static str,
    wrapper: &'static str,
    fields: &'static [Field],
    signatures: SignatureMode,
    stores_hash: bool,
}

macro_rules! f {
    ($name:literal, $kind:expr) => {
        Field {
            name: $name,
            kind: $kind,
        }
    };
}

const TRANSFER: &[Field] = &[
    f!("txtype", FieldKind::U8),
    f!("time", FieldKind::U32),
    f!("value", FieldKind::U64),
    f!("coin", FieldKind::PaddedText(15)),
    f!("nft_series", FieldKind::U32),
    f!("sender", FieldKind::Address),
    f!("receiver", FieldKind::Address),
    f!("txfee", FieldKind::U64),
];
const TOKEN: &[Field] = &[
    f!("txtype", FieldKind::U8),
    f!("time", FieldKind::U32),
    f!("creator", FieldKind::Address),
    f!("ticker", FieldKind::PaddedText(15)),
    f!("number", FieldKind::U64),
    f!("hard_limit", FieldKind::U8),
    f!("txfee", FieldKind::U64),
];
const NFT: &[Field] = &[
    f!("txtype", FieldKind::U8),
    f!("time", FieldKind::U32),
    f!("creator", FieldKind::Address),
    f!("series", FieldKind::U8),
    f!("ownership_type", FieldKind::U8),
    f!("nft_name", FieldKind::PaddedText(15)),
    f!("item_ipfs", FieldKind::PaddedText(100)),
    f!("count", FieldKind::U32),
    f!("desc", FieldKind::PaddedText(100)),
    f!("txfee", FieldKind::U64),
];
const MARKETING: &[Field] = &[
    f!("txtype", FieldKind::U8),
    f!("time", FieldKind::U32),
    f!("campaign", FieldKind::U64),
    f!("ad_type", FieldKind::PaddedText(6)),
    f!("keyword", FieldKind::PaddedText(40)),
    f!("displayed", FieldKind::PaddedText(100)),
    f!("impression", FieldKind::U8),
    f!("click", FieldKind::U8),
    f!("impression_value", FieldKind::U16),
    f!("click_value", FieldKind::U16),
    f!("advertiser", FieldKind::Address),
    f!("txfee", FieldKind::U64),
];
const SWAP: &[Field] = &[
    f!("txtype", FieldKind::U8),
    f!("timestamp", FieldKind::U32),
    f!("offer_expiration", FieldKind::U32),
    f!("ticker1", FieldKind::PaddedText(15)),
    f!("nft_series1", FieldKind::U32),
    f!("value1", FieldKind::U64),
    f!("ticker2", FieldKind::PaddedText(15)),
    f!("nft_series2", FieldKind::U32),
    f!("value2", FieldKind::U64),
    f!("sender1", FieldKind::Address),
    f!("sender2", FieldKind::Address),
    f!("tip1", FieldKind::U64),
    f!("tip2", FieldKind::U64),
    f!("txfee1", FieldKind::U64),
    f!("txfee2", FieldKind::U64),
];
const LOAN: &[Field] = &[
    f!("txtype", FieldKind::U8),
    f!("timestamp", FieldKind::U32),
    f!("loan_coin", FieldKind::PaddedText(15)),
    f!("loan_amount", FieldKind::U64),
    f!("lender", FieldKind::Address),
    f!("collateral", FieldKind::PaddedText(21)),
    f!("collateral_amount", FieldKind::U64),
    f!("borrower", FieldKind::Address),
    f!("payment_period", FieldKind::Text(1)),
    f!("payment_number", FieldKind::U8),
    f!("payment_amount", FieldKind::U64),
    f!("grace_period", FieldKind::U8),
    f!("max_late_value", FieldKind::U64),
    f!("txfee", FieldKind::U64),
];
const LOAN_PAYMENT: &[Field] = &[
    f!("txtype", FieldKind::U8),
    f!("timestamp", FieldKind::U32),
    f!("payback_amount", FieldKind::U64),
    f!("contract_hash", FieldKind::Hash(32)),
    f!("address", FieldKind::Address),
    f!("tip", FieldKind::U64),
    f!("txfee", FieldKind::U64),
];
const COLLATERAL: &[Field] = &[
    f!("txtype", FieldKind::U8),
    f!("time", FieldKind::U32),
    f!("contract_hash", FieldKind::Hash(32)),
    f!("address", FieldKind::Address),
    f!("txfee", FieldKind::U64),
];
const BURN: &[Field] = &[
    f!("txtype", FieldKind::U8),
    f!("time", FieldKind::U32),
    f!("address", FieldKind::Address),
    f!("coin", FieldKind::PaddedText(15)),
    f!("nft_series", FieldKind::U32),
    f!("value", FieldKind::U64),
    f!("txfee", FieldKind::U64),
];
const ISSUE_TOKEN: &[Field] = &[
    f!("txtype", FieldKind::U8),
    f!("time", FieldKind::U32),
    f!("creator", FieldKind::Address),
    f!("ticker", FieldKind::PaddedText(15)),
    f!("number", FieldKind::U64),
    f!("txfee", FieldKind::U64),
];
const VANITY: &[Field] = &[
    f!("txtype", FieldKind::U8),
    f!("timestamp", FieldKind::U32),
    f!("address", FieldKind::Address),
    f!("vanity_address", FieldKind::VanityAddress),
    f!("txfee", FieldKind::U64),
];
const STORAGE_KEY: &[Field] = &[
    f!("txtype", FieldKind::U8),
    f!("time", FieldKind::U32),
    f!("address", FieldKind::Address),
    f!("txfee", FieldKind::U64),
];

macro_rules! storage_fields {
    ($kind:expr) => {
        &[
            f!("txtype", FieldKind::U8),
            f!("time", FieldKind::U32),
            f!("storagekey", FieldKind::Hash(32)),
            f!("key", FieldKind::PaddedText(50)),
            f!("value", $kind),
            f!("address", FieldKind::Address),
            f!("txfee", FieldKind::U64),
        ]
    };
}

const STORAGE_BOOL: &[Field] = storage_fields!(FieldKind::Bool);
const STORAGE_U8: &[Field] = storage_fields!(FieldKind::U8);
const STORAGE_U16: &[Field] = storage_fields!(FieldKind::U16);
const STORAGE_U32: &[Field] = storage_fields!(FieldKind::U32);
const STORAGE_U64: &[Field] = storage_fields!(FieldKind::U64);
const STORAGE_U128: &[Field] = storage_fields!(FieldKind::U128);
const STORAGE_I8: &[Field] = storage_fields!(FieldKind::I8);
const STORAGE_I16: &[Field] = storage_fields!(FieldKind::I16);
const STORAGE_I32: &[Field] = storage_fields!(FieldKind::I32);
const STORAGE_I64: &[Field] = storage_fields!(FieldKind::I64);
const STORAGE_I128: &[Field] = storage_fields!(FieldKind::I128);
const STORAGE_STRING: &[Field] = &[
    f!("txtype", FieldKind::U8),
    f!("time", FieldKind::U32),
    f!("storagekey", FieldKind::Hash(32)),
    f!("key", FieldKind::PaddedText(50)),
    f!("value", FieldKind::PaddedText(180)),
    f!("address", FieldKind::Address),
    f!("previous", FieldKind::Hash(32)),
    f!("txfee", FieldKind::U64),
];
const DELETE_KEY: &[Field] = &[
    f!("txtype", FieldKind::U8),
    f!("time", FieldKind::U32),
    f!("storagekey", FieldKind::Hash(32)),
    f!("key", FieldKind::PaddedText(50)),
    f!("address", FieldKind::Address),
    f!("txfee", FieldKind::U64),
];
const PROPOSAL_KEY: &[Field] = &[
    f!("txtype", FieldKind::U8),
    f!("time", FieldKind::U32),
    f!("proposal_hash", FieldKind::Hash(32)),
    f!("address", FieldKind::Address),
    f!("txfee", FieldKind::U64),
];
const PROPOSAL_VOTE: &[Field] = &[
    f!("txtype", FieldKind::U8),
    f!("time", FieldKind::U32),
    f!("proposal_key", FieldKind::Hash(32)),
    f!("address", FieldKind::Address),
    f!("vote", FieldKind::U8),
    f!("txfee", FieldKind::U64),
];
const ACTIVATION_VOTE: &[Field] = &[
    f!("txtype", FieldKind::U8),
    f!("time", FieldKind::U32),
    f!("proposal_key", FieldKind::Hash(32)),
    f!("development_hash", FieldKind::Hash(32)),
    f!("development_location", FieldKind::PaddedText(100)),
    f!("address", FieldKind::Address),
    f!("vote", FieldKind::U8),
    f!("txfee", FieldKind::U64),
];

const SCHEMAS: &[Schema] = &[
    Schema {
        txtype: 2,
        name: "Transfer",
        wrapper: "unsigned_transfer",
        fields: TRANSFER,
        signatures: SignatureMode::Single,
        stores_hash: false,
    },
    Schema {
        txtype: 3,
        name: "Create Token",
        wrapper: "unsigned_create_token",
        fields: TOKEN,
        signatures: SignatureMode::Single,
        stores_hash: false,
    },
    Schema {
        txtype: 4,
        name: "Create NFT/RWA",
        wrapper: "unsigned_create_nft",
        fields: NFT,
        signatures: SignatureMode::Single,
        stores_hash: false,
    },
    Schema {
        txtype: 5,
        name: "Marketing",
        wrapper: "unsigned_marketing",
        fields: MARKETING,
        signatures: SignatureMode::Single,
        stores_hash: false,
    },
    Schema {
        txtype: 6,
        name: "Swap",
        wrapper: "unsigned_swap",
        fields: SWAP,
        signatures: SignatureMode::Dual,
        stores_hash: false,
    },
    Schema {
        txtype: 7,
        name: "Loan",
        wrapper: "unsigned_loan_contract",
        fields: LOAN,
        signatures: SignatureMode::Dual,
        stores_hash: true,
    },
    Schema {
        txtype: 8,
        name: "Loan Payment",
        wrapper: "unsigned_contract_payment",
        fields: LOAN_PAYMENT,
        signatures: SignatureMode::Single,
        stores_hash: true,
    },
    Schema {
        txtype: 9,
        name: "Collateral Claim",
        wrapper: "unsigned_collateral_claim",
        fields: COLLATERAL,
        signatures: SignatureMode::Single,
        stores_hash: false,
    },
    Schema {
        txtype: 10,
        name: "Burn",
        wrapper: "unsigned_burn",
        fields: BURN,
        signatures: SignatureMode::Single,
        stores_hash: false,
    },
    Schema {
        txtype: 11,
        name: "Issue Token",
        wrapper: "unsigned_issue_token",
        fields: ISSUE_TOKEN,
        signatures: SignatureMode::Single,
        stores_hash: false,
    },
    Schema {
        txtype: 12,
        name: "Vanity Address",
        wrapper: "unsigned_vanity_address",
        fields: VANITY,
        signatures: SignatureMode::Single,
        stores_hash: false,
    },
    Schema {
        txtype: 100,
        name: "Storage Key",
        wrapper: "unsigned_storagekey",
        fields: STORAGE_KEY,
        signatures: SignatureMode::Single,
        stores_hash: false,
    },
    Schema {
        txtype: 101,
        name: "Storage Bool",
        wrapper: "unsigned_storagebool",
        fields: STORAGE_BOOL,
        signatures: SignatureMode::Single,
        stores_hash: false,
    },
    Schema {
        txtype: 102,
        name: "Storage U8",
        wrapper: "unsigned_storageu8",
        fields: STORAGE_U8,
        signatures: SignatureMode::Single,
        stores_hash: false,
    },
    Schema {
        txtype: 103,
        name: "Storage U16",
        wrapper: "unsigned_storageu16",
        fields: STORAGE_U16,
        signatures: SignatureMode::Single,
        stores_hash: false,
    },
    Schema {
        txtype: 104,
        name: "Storage U32",
        wrapper: "unsigned_storageu32",
        fields: STORAGE_U32,
        signatures: SignatureMode::Single,
        stores_hash: false,
    },
    Schema {
        txtype: 105,
        name: "Storage U64",
        wrapper: "unsigned_storageu64",
        fields: STORAGE_U64,
        signatures: SignatureMode::Single,
        stores_hash: false,
    },
    Schema {
        txtype: 106,
        name: "Storage U128",
        wrapper: "unsigned_storageu128",
        fields: STORAGE_U128,
        signatures: SignatureMode::Single,
        stores_hash: false,
    },
    Schema {
        txtype: 107,
        name: "Storage String",
        wrapper: "unsigned_storage",
        fields: STORAGE_STRING,
        signatures: SignatureMode::Single,
        stores_hash: false,
    },
    Schema {
        txtype: 108,
        name: "Storage I8",
        wrapper: "unsigned_storagei8",
        fields: STORAGE_I8,
        signatures: SignatureMode::Single,
        stores_hash: false,
    },
    Schema {
        txtype: 109,
        name: "Storage I16",
        wrapper: "unsigned_storagei16",
        fields: STORAGE_I16,
        signatures: SignatureMode::Single,
        stores_hash: false,
    },
    Schema {
        txtype: 110,
        name: "Storage I32",
        wrapper: "unsigned_storagei32",
        fields: STORAGE_I32,
        signatures: SignatureMode::Single,
        stores_hash: false,
    },
    Schema {
        txtype: 111,
        name: "Storage I64",
        wrapper: "unsigned_storagei64",
        fields: STORAGE_I64,
        signatures: SignatureMode::Single,
        stores_hash: false,
    },
    Schema {
        txtype: 112,
        name: "Storage I128",
        wrapper: "unsigned_storagei128",
        fields: STORAGE_I128,
        signatures: SignatureMode::Single,
        stores_hash: false,
    },
    Schema {
        txtype: 113,
        name: "Delete Storage Key",
        wrapper: "unsigned_deletekey",
        fields: DELETE_KEY,
        signatures: SignatureMode::Single,
        stores_hash: false,
    },
    Schema {
        txtype: 200,
        name: "Proposal Key",
        wrapper: "unsigned_proposalkey",
        fields: PROPOSAL_KEY,
        signatures: SignatureMode::Single,
        stores_hash: false,
    },
    Schema {
        txtype: 201,
        name: "Proposal Vote",
        wrapper: "unsigned_proposalvote",
        fields: PROPOSAL_VOTE,
        signatures: SignatureMode::Single,
        stores_hash: false,
    },
    Schema {
        txtype: 202,
        name: "Activation Vote",
        wrapper: "unsigned_activationvote",
        fields: ACTIVATION_VOTE,
        signatures: SignatureMode::Single,
        stores_hash: false,
    },
];

#[derive(Serialize)]
pub struct Inspection {
    pub txtype: u8,
    pub transaction_name: &'static str,
    pub canonical_unsigned_json: String,
    pub hash: String,
    pub unsigned_bytes_hex: String,
    pub required_signatures: u8,
    pub review: Value,
}

#[derive(Serialize)]
pub struct RegistryEntry {
    pub txtype: u8,
    pub transaction_name: &'static str,
    pub byte_length: usize,
    pub required_signatures: u8,
    pub signer1_field: &'static str,
    pub signer2_field: Option<&'static str>,
}

#[derive(Serialize)]
pub struct SigningResult {
    pub complete: bool,
    pub txtype: u8,
    pub transaction_name: &'static str,
    pub hash: String,
    pub unsigned_bytes_hex: String,
    pub signature1: Option<String>,
    pub signature2: Option<String>,
    pub signed_transaction: Value,
    pub bytes_hex: Option<String>,
}

#[derive(Serialize)]
pub struct DecodedTransaction {
    pub txtype: u8,
    pub transaction_name: &'static str,
    pub fields: Value,
    pub stored_hash: Option<String>,
    pub signature: Option<String>,
    pub signature1: Option<String>,
    pub signature2: Option<String>,
}

fn schema(txtype: u8) -> Result<&'static Schema, BrowserCoreError> {
    SCHEMAS
        .iter()
        .find(|schema| schema.txtype == txtype)
        .ok_or(BrowserCoreError::UnsupportedTransaction(txtype))
}

fn request_object(request: &Value) -> Result<&Map<String, Value>, BrowserCoreError> {
    request
        .as_object()
        .ok_or_else(|| BrowserCoreError::InvalidRequest("request must be a JSON object".into()))
}

fn request_source<'a>(
    request: &'a Value,
    schema: &Schema,
) -> Result<&'a Map<String, Value>, BrowserCoreError> {
    let object = request_object(request)?;
    match object.get(schema.wrapper) {
        Some(Value::Object(unsigned)) => Ok(unsigned),
        Some(_) => Err(BrowserCoreError::InvalidRequest(format!(
            "{} must be a JSON object",
            schema.wrapper
        ))),
        None => Ok(object),
    }
}

fn request_type(request: &Value) -> Result<u8, BrowserCoreError> {
    let object = request_object(request)?;
    if let Some(value) = object.get("txtype") {
        return parse_integer::<u8>(value, "txtype");
    }
    for schema in SCHEMAS {
        if let Some(Value::Object(unsigned)) = object.get(schema.wrapper) {
            let value = unsigned
                .get("txtype")
                .ok_or_else(|| BrowserCoreError::InvalidRequest("txtype is required".into()))?;
            return parse_integer::<u8>(value, "txtype");
        }
    }
    Err(BrowserCoreError::InvalidRequest(
        "txtype is required".into(),
    ))
}

fn canonical_unsigned(request: &Value, schema: &Schema) -> Result<Value, BrowserCoreError> {
    let source = request_source(request, schema)?;
    let allowed_extra = ["signature", "signature1", "signature2", "hash"];
    for key in source.keys() {
        if !schema.fields.iter().any(|field| field.name == key)
            && !allowed_extra.contains(&key.as_str())
        {
            return Err(BrowserCoreError::InvalidRequest(format!(
                "unexpected field: {key}"
            )));
        }
    }
    let mut canonical = Map::new();
    for field in schema.fields {
        let value = source.get(field.name).ok_or_else(|| {
            BrowserCoreError::InvalidRequest(format!("missing field: {}", field.name))
        })?;
        canonical.insert(field.name.into(), canonical_value(value, field)?);
    }
    if canonical.get("txtype").and_then(Value::as_u64) != Some(schema.txtype as u64) {
        return Err(BrowserCoreError::InvalidRequest(format!(
            "txtype must be {}",
            schema.txtype
        )));
    }
    let canonical = Value::Object(canonical);
    validate_semantics(schema, &canonical)?;
    Ok(canonical)
}

fn validate_semantics(schema: &Schema, unsigned: &Value) -> Result<(), BrowserCoreError> {
    let object = request_object(unsigned)?;
    let byte_flag = |name: &str| {
        let value = object.get(name).and_then(Value::as_u64);
        if matches!(value, Some(0 | 1)) {
            Ok(())
        } else {
            Err(BrowserCoreError::InvalidRequest(format!(
                "{name} must be 0 or 1"
            )))
        }
    };

    match schema.txtype {
        2 => {
            if object.get("sender") == object.get("receiver") {
                return Err(BrowserCoreError::InvalidRequest(
                    "sender and receiver cannot be the same".into(),
                ));
            }
            if object
                .get("nft_series")
                .and_then(Value::as_u64)
                .unwrap_or(0)
                > 0
                && object.get("value").and_then(Value::as_u64) != Some(100_000_000)
            {
                return Err(BrowserCoreError::InvalidRequest(
                    "series NFT transfers must have a value of 100000000".into(),
                ));
            }
        }
        3 => byte_flag("hard_limit")?,
        4 => {
            byte_flag("series")?;
            byte_flag("ownership_type")?;
            if object.get("count").and_then(Value::as_u64) == Some(0) {
                return Err(BrowserCoreError::InvalidRequest(
                    "NFT count cannot be zero".into(),
                ));
            }
        }
        6 => {
            if object.get("sender1") == object.get("sender2") {
                return Err(BrowserCoreError::InvalidRequest(
                    "swap participants cannot be the same".into(),
                ));
            }
            if object.get("offer_expiration").and_then(Value::as_u64)
                <= object.get("timestamp").and_then(Value::as_u64)
            {
                return Err(BrowserCoreError::InvalidRequest(
                    "swap expiration must be after its timestamp".into(),
                ));
            }
        }
        7 => {
            if object.get("lender") == object.get("borrower") {
                return Err(BrowserCoreError::InvalidRequest(
                    "lender and borrower cannot be the same".into(),
                ));
            }
            if !matches!(
                object.get("payment_period").and_then(Value::as_str),
                Some("d" | "w" | "m")
            ) {
                return Err(BrowserCoreError::InvalidRequest(
                    "payment_period must be d, w, or m".into(),
                ));
            }
        }
        201 | 202 => byte_flag("vote")?,
        _ => {}
    }
    Ok(())
}

fn canonical_value(value: &Value, field: &Field) -> Result<Value, BrowserCoreError> {
    let invalid = || BrowserCoreError::InvalidRequest(format!("invalid {}", field.name));
    match field.kind {
        FieldKind::Bool => value.as_bool().map(Value::Bool).ok_or_else(invalid),
        FieldKind::U8 => number_unsigned::<u8>(value, field.name),
        FieldKind::U16 => number_unsigned::<u16>(value, field.name),
        FieldKind::U32 => number_unsigned::<u32>(value, field.name),
        FieldKind::U64 => number_unsigned::<u64>(value, field.name),
        FieldKind::U128 => number_from_text::<u128>(value, field.name),
        FieldKind::I8 => number_signed::<i8>(value, field.name),
        FieldKind::I16 => number_signed::<i16>(value, field.name),
        FieldKind::I32 => number_signed::<i32>(value, field.name),
        FieldKind::I64 => number_signed::<i64>(value, field.name),
        FieldKind::I128 => number_from_text::<i128>(value, field.name),
        FieldKind::Address => {
            let text = value.as_str().ok_or_else(invalid)?;
            address::short_address_to_bytes(text)?;
            Ok(Value::String(text.into()))
        }
        FieldKind::VanityAddress => {
            let text = value.as_str().ok_or_else(invalid)?;
            Ok(Value::String(address::normalize_vanity_address(text)?))
        }
        FieldKind::Hash(length) => {
            let text = value.as_str().ok_or_else(invalid)?;
            let bytes = hex::decode(text).map_err(|_| invalid())?;
            if bytes.len() != length {
                return Err(invalid());
            }
            Ok(Value::String(text.to_ascii_lowercase()))
        }
        FieldKind::Text(length) => {
            let text = value.as_str().ok_or_else(invalid)?;
            if !text.is_ascii() || text.len() != length {
                return Err(invalid());
            }
            Ok(Value::String(text.into()))
        }
        FieldKind::PaddedText(length) => {
            let text = value.as_str().ok_or_else(invalid)?.trim();
            if text.is_empty() || !text.is_ascii() || text.len() > length {
                return Err(invalid());
            }
            Ok(Value::String(format!("{text:<length$}")))
        }
    }
}

fn parse_integer<T: FromStr>(value: &Value, name: &str) -> Result<T, BrowserCoreError> {
    let text = match value {
        Value::String(text) => text.clone(),
        Value::Number(number) => number.to_string(),
        _ => {
            return Err(BrowserCoreError::InvalidRequest(format!(
                "{name} must be an integer"
            )))
        }
    };
    text.parse::<T>()
        .map_err(|_| BrowserCoreError::InvalidRequest(format!("invalid {name}")))
}

fn number_unsigned<T>(value: &Value, name: &str) -> Result<Value, BrowserCoreError>
where
    T: FromStr + Into<u128>,
{
    let parsed = parse_integer::<T>(value, name)?;
    let number = Number::from_str(&parsed.into().to_string())
        .map_err(|_| BrowserCoreError::InvalidRequest(format!("invalid {name}")))?;
    Ok(Value::Number(number))
}

fn number_signed<T>(value: &Value, name: &str) -> Result<Value, BrowserCoreError>
where
    T: FromStr + Into<i128>,
{
    let parsed = parse_integer::<T>(value, name)?;
    let number = Number::from_str(&parsed.into().to_string())
        .map_err(|_| BrowserCoreError::InvalidRequest(format!("invalid {name}")))?;
    Ok(Value::Number(number))
}

fn number_from_text<T: FromStr + ToString>(
    value: &Value,
    name: &str,
) -> Result<Value, BrowserCoreError> {
    let parsed = parse_integer::<T>(value, name)?;
    let number = Number::from_str(&parsed.to_string())
        .map_err(|_| BrowserCoreError::InvalidRequest(format!("invalid {name}")))?;
    Ok(Value::Number(number))
}

pub fn inspect(request: &Value) -> Result<Inspection, BrowserCoreError> {
    let txtype = request_type(request)?;
    let schema = schema(txtype)?;
    let unsigned = canonical_unsigned(request, schema)?;
    let canonical_unsigned_json = serde_json::to_string(&unsigned)
        .map_err(|error| BrowserCoreError::Json(error.to_string()))?;
    let hash = hex::encode(crypto::skein_256(canonical_unsigned_json.as_bytes()));
    let unsigned_bytes_hex = hex::encode(encode_unsigned_prefix(schema, &unsigned, &hash)?);
    Ok(Inspection {
        txtype,
        transaction_name: schema.name,
        canonical_unsigned_json,
        hash,
        unsigned_bytes_hex,
        required_signatures: if schema.signatures == SignatureMode::Dual {
            2
        } else {
            1
        },
        review: unsigned,
    })
}

pub fn sign_request(
    request: &Value,
    private_key: &[u8],
    signer_public_key: &[u8],
    signer_slot: u8,
) -> Result<SigningResult, BrowserCoreError> {
    if private_key.len() != PRIVATE_KEY_LENGTH {
        return Err(BrowserCoreError::InvalidPrivateKey);
    }
    let inspection = inspect(request)?;
    let schema = schema(inspection.txtype)?;
    let source = request_object(request)?;
    let signature_source = if source.contains_key(schema.wrapper) {
        source
    } else {
        request_source(request, schema)?
    };
    let unsigned: Value = serde_json::from_str(&inspection.canonical_unsigned_json)
        .map_err(|error| BrowserCoreError::Json(error.to_string()))?;
    let signer_address = address::public_key_to_short_address(signer_public_key)?;
    validate_signer(schema, &unsigned, &signer_address, signer_slot)?;
    let hash_bytes = hex::decode(&inspection.hash).unwrap();
    let new_signature_bytes = crypto::sign(&hash_bytes, private_key)?;
    if !crypto::verify(&hash_bytes, &new_signature_bytes, signer_public_key) {
        return Err(BrowserCoreError::InvalidRequest(
            "private key does not match the wallet public key".into(),
        ));
    }
    let new_signature = hex::encode(new_signature_bytes);
    let mut signature1 = signature_source
        .get("signature1")
        .or_else(|| source.get("signature"))
        .and_then(Value::as_str)
        .map(str::to_owned);
    let mut signature2 = signature_source
        .get("signature2")
        .and_then(Value::as_str)
        .map(str::to_owned);

    if schema.signatures == SignatureMode::Dual && (signature1.is_some() || signature2.is_some()) {
        return Err(BrowserCoreError::InvalidRequest(
            "dual-signature wallets must sign the unsigned transaction independently".into(),
        ));
    }

    match schema.signatures {
        SignatureMode::Single => {
            if signer_slot != 1 {
                return Err(BrowserCoreError::InvalidRequest(
                    "single-signature transactions use signer slot 1".into(),
                ));
            }
            signature1 = Some(new_signature);
        }
        SignatureMode::Dual => match signer_slot {
            1 if signature1.is_none() => signature1 = Some(new_signature),
            2 if signature2.is_none() => signature2 = Some(new_signature),
            1 => {
                return Err(BrowserCoreError::InvalidRequest(
                    "signature1 already exists".into(),
                ))
            }
            2 => {
                return Err(BrowserCoreError::InvalidRequest(
                    "signature2 already exists".into(),
                ))
            }
            _ => {
                return Err(BrowserCoreError::InvalidRequest(
                    "signer slot must be 1 or 2".into(),
                ))
            }
        },
    }

    if signature1.is_some() {
        validate_signature(signature1.as_deref())?;
    }
    if signature2.is_some() {
        validate_signature(signature2.as_deref())?;
    }
    let complete = signature1.is_some()
        && (schema.signatures == SignatureMode::Single || signature2.is_some());
    let mut signed = Map::new();
    signed.insert(schema.wrapper.into(), unsigned.clone());
    if schema.stores_hash {
        signed.insert("hash".into(), Value::String(inspection.hash.clone()));
    }
    if schema.signatures == SignatureMode::Single {
        signed.insert(
            "signature".into(),
            Value::String(signature1.clone().unwrap()),
        );
    } else {
        if let Some(value) = &signature1 {
            signed.insert("signature1".into(), Value::String(value.clone()));
        }
        if let Some(value) = &signature2 {
            signed.insert("signature2".into(), Value::String(value.clone()));
        }
    }
    let bytes_hex = if complete {
        Some(hex::encode(encode_transaction(
            schema,
            &unsigned,
            &inspection.hash,
            signature1.as_deref(),
            signature2.as_deref(),
        )?))
    } else {
        None
    };
    Ok(SigningResult {
        complete,
        txtype: schema.txtype,
        transaction_name: schema.name,
        hash: inspection.hash,
        unsigned_bytes_hex: inspection.unsigned_bytes_hex,
        signature1,
        signature2,
        signed_transaction: Value::Object(signed),
        bytes_hex,
    })
}

pub fn complete_dual_request(
    request: &Value,
    signature1: &str,
    public_key1: &[u8],
    signature2: &str,
    public_key2: &[u8],
) -> Result<SigningResult, BrowserCoreError> {
    let inspection = inspect(request)?;
    let schema = schema(inspection.txtype)?;
    if schema.signatures != SignatureMode::Dual {
        return Err(BrowserCoreError::InvalidRequest(
            "only swaps and loan creation use dual signatures".into(),
        ));
    }
    validate_signature(Some(signature1))?;
    validate_signature(Some(signature2))?;
    let unsigned: Value = serde_json::from_str(&inspection.canonical_unsigned_json)
        .map_err(|error| BrowserCoreError::Json(error.to_string()))?;
    let address1 = address::public_key_to_short_address(public_key1)?;
    let address2 = address::public_key_to_short_address(public_key2)?;
    validate_signer(schema, &unsigned, &address1, 1)?;
    validate_signer(schema, &unsigned, &address2, 2)?;
    let hash_bytes = hex::decode(&inspection.hash).unwrap();
    let signature1_bytes =
        hex::decode(signature1).map_err(|_| BrowserCoreError::InvalidSignature)?;
    let signature2_bytes =
        hex::decode(signature2).map_err(|_| BrowserCoreError::InvalidSignature)?;
    if !crypto::verify(&hash_bytes, &signature1_bytes, public_key1) {
        return Err(BrowserCoreError::InvalidRequest(
            "signature1 is not valid for signer 1".into(),
        ));
    }
    if !crypto::verify(&hash_bytes, &signature2_bytes, public_key2) {
        return Err(BrowserCoreError::InvalidRequest(
            "signature2 is not valid for signer 2".into(),
        ));
    }
    let mut signed = Map::new();
    signed.insert(schema.wrapper.into(), unsigned.clone());
    if schema.stores_hash {
        signed.insert("hash".into(), Value::String(inspection.hash.clone()));
    }
    signed.insert("signature1".into(), Value::String(signature1.into()));
    signed.insert("signature2".into(), Value::String(signature2.into()));
    let bytes_hex = hex::encode(encode_transaction(
        schema,
        &unsigned,
        &inspection.hash,
        Some(signature1),
        Some(signature2),
    )?);
    Ok(SigningResult {
        complete: true,
        txtype: schema.txtype,
        transaction_name: schema.name,
        hash: inspection.hash,
        unsigned_bytes_hex: inspection.unsigned_bytes_hex,
        signature1: Some(signature1.into()),
        signature2: Some(signature2.into()),
        signed_transaction: Value::Object(signed),
        bytes_hex: Some(bytes_hex),
    })
}

pub fn entries() -> Vec<RegistryEntry> {
    SCHEMAS
        .iter()
        .map(|schema| RegistryEntry {
            txtype: schema.txtype,
            transaction_name: schema.name,
            byte_length: wire_length(schema),
            required_signatures: if schema.signatures == SignatureMode::Dual {
                2
            } else {
                1
            },
            signer1_field: signer_field(schema.txtype, 1).unwrap(),
            signer2_field: signer_field(schema.txtype, 2),
        })
        .collect()
}

pub fn decode_transaction(bytes: &[u8]) -> Result<DecodedTransaction, BrowserCoreError> {
    let txtype = *bytes
        .first()
        .ok_or_else(|| BrowserCoreError::InvalidRequest("transaction bytes were empty".into()))?;
    let schema = schema(txtype)?;
    if bytes.len() != wire_length(schema) {
        return Err(BrowserCoreError::InvalidRequest(format!(
            "{} transaction must contain exactly {} bytes",
            schema.name,
            wire_length(schema)
        )));
    }

    let mut offset = 0usize;
    let mut fields = Map::new();
    for field in schema.fields {
        fields.insert(field.name.into(), decode_field(bytes, &mut offset, field)?);
    }

    let stored_hash = if schema.stores_hash {
        Some(hex::encode(read_bytes(bytes, &mut offset, 32)?))
    } else {
        None
    };
    let first_signature = hex::encode(read_bytes(bytes, &mut offset, SIGNATURE_LENGTH)?);
    let (signature, signature1, signature2) = if schema.signatures == SignatureMode::Dual {
        let second_signature = hex::encode(read_bytes(bytes, &mut offset, SIGNATURE_LENGTH)?);
        (None, Some(first_signature), Some(second_signature))
    } else {
        (Some(first_signature), None, None)
    };

    if offset != bytes.len() {
        return Err(BrowserCoreError::InvalidRequest(
            "transaction contained trailing bytes".into(),
        ));
    }

    Ok(DecodedTransaction {
        txtype,
        transaction_name: schema.name,
        fields: Value::Object(fields),
        stored_hash,
        signature,
        signature1,
        signature2,
    })
}

fn read_bytes<'a>(
    bytes: &'a [u8],
    offset: &mut usize,
    length: usize,
) -> Result<&'a [u8], BrowserCoreError> {
    let end = offset.checked_add(length).ok_or_else(|| {
        BrowserCoreError::InvalidRequest("transaction field offset overflowed".into())
    })?;
    let value = bytes.get(*offset..end).ok_or_else(|| {
        BrowserCoreError::InvalidRequest("transaction bytes were truncated".into())
    })?;
    *offset = end;
    Ok(value)
}

fn decode_field(
    bytes: &[u8],
    offset: &mut usize,
    field: &Field,
) -> Result<Value, BrowserCoreError> {
    let raw = read_bytes(bytes, offset, field_width(field.kind))?;
    let invalid = || BrowserCoreError::InvalidRequest(format!("invalid encoded {}", field.name));
    Ok(match field.kind {
        FieldKind::Bool => match raw[0] {
            0 => Value::Bool(false),
            1 => Value::Bool(true),
            _ => return Err(invalid()),
        },
        FieldKind::U8 => Value::Number(Number::from(raw[0])),
        FieldKind::U16 => Value::Number(Number::from(u16::from_le_bytes(
            raw.try_into().map_err(|_| invalid())?,
        ))),
        FieldKind::U32 => Value::Number(Number::from(u32::from_le_bytes(
            raw.try_into().map_err(|_| invalid())?,
        ))),
        FieldKind::U64 => {
            Value::String(u64::from_le_bytes(raw.try_into().map_err(|_| invalid())?).to_string())
        }
        FieldKind::U128 => {
            Value::String(u128::from_le_bytes(raw.try_into().map_err(|_| invalid())?).to_string())
        }
        FieldKind::I8 => Value::Number(Number::from(raw[0] as i8)),
        FieldKind::I16 => Value::Number(Number::from(i16::from_le_bytes(
            raw.try_into().map_err(|_| invalid())?,
        ))),
        FieldKind::I32 => Value::Number(Number::from(i32::from_le_bytes(
            raw.try_into().map_err(|_| invalid())?,
        ))),
        FieldKind::I64 => {
            Value::String(i64::from_le_bytes(raw.try_into().map_err(|_| invalid())?).to_string())
        }
        FieldKind::I128 => {
            Value::String(i128::from_le_bytes(raw.try_into().map_err(|_| invalid())?).to_string())
        }
        FieldKind::Address => Value::String(address::bytes_to_short_address(raw)?),
        FieldKind::VanityAddress => {
            if raw.len() != 22 || raw[20] != b'.' || raw[21] != address::network_byte() {
                return Err(invalid());
            }
            let name = std::str::from_utf8(&raw[..20])
                .map_err(|_| invalid())?
                .trim()
                .to_string();
            Value::String(format!("{name}.{}", address::network_suffix()))
        }
        FieldKind::Hash(_) => Value::String(hex::encode(raw)),
        FieldKind::Text(_) | FieldKind::PaddedText(_) => {
            let text = std::str::from_utf8(raw).map_err(|_| invalid())?;
            Value::String(
                text.trim_end_matches(|character| character == '\0' || character == ' ')
                    .to_string(),
            )
        }
    })
}

fn validate_signer(
    schema: &Schema,
    unsigned: &Value,
    signer_address: &str,
    signer_slot: u8,
) -> Result<(), BrowserCoreError> {
    address::short_address_to_bytes(signer_address)?;
    let field = signer_field(schema.txtype, signer_slot).ok_or_else(|| {
        BrowserCoreError::InvalidRequest(format!(
            "transaction type {} does not use signer slot {signer_slot}",
            schema.txtype
        ))
    })?;
    let expected = request_object(unsigned)?
        .get(field)
        .and_then(Value::as_str)
        .ok_or_else(|| {
            BrowserCoreError::InvalidRequest(format!("missing signer field: {field}"))
        })?;
    if !expected.eq_ignore_ascii_case(signer_address) {
        return Err(BrowserCoreError::InvalidRequest(format!(
            "wallet address does not match {field}"
        )));
    }
    Ok(())
}

fn signer_field(txtype: u8, slot: u8) -> Option<&'static str> {
    match (txtype, slot) {
        (2, 1) => Some("sender"),
        (3 | 4 | 11, 1) => Some("creator"),
        (5, 1) => Some("advertiser"),
        (6, 1) => Some("sender1"),
        (6, 2) => Some("sender2"),
        (7, 1) => Some("lender"),
        (7, 2) => Some("borrower"),
        (8 | 9 | 10 | 12 | 100..=113 | 200..=202, 1) => Some("address"),
        _ => None,
    }
}

fn validate_signature(signature: Option<&str>) -> Result<(), BrowserCoreError> {
    let signature = signature.ok_or(BrowserCoreError::InvalidSignature)?;
    if hex::decode(signature).map_or(true, |bytes| bytes.len() != SIGNATURE_LENGTH) {
        return Err(BrowserCoreError::InvalidSignature);
    }
    Ok(())
}

fn encode_transaction(
    schema: &Schema,
    unsigned: &Value,
    hash: &str,
    signature1: Option<&str>,
    signature2: Option<&str>,
) -> Result<Vec<u8>, BrowserCoreError> {
    let mut bytes = encode_unsigned_prefix(schema, unsigned, hash)?;
    bytes.extend_from_slice(
        &hex::decode(signature1.ok_or(BrowserCoreError::InvalidSignature)?).unwrap(),
    );
    if schema.signatures == SignatureMode::Dual {
        bytes.extend_from_slice(
            &hex::decode(signature2.ok_or(BrowserCoreError::InvalidSignature)?).unwrap(),
        );
    }
    Ok(bytes)
}

fn encode_unsigned_prefix(
    schema: &Schema,
    unsigned: &Value,
    hash: &str,
) -> Result<Vec<u8>, BrowserCoreError> {
    let object = request_object(unsigned)?;
    let mut bytes = Vec::new();
    for field in schema.fields {
        encode_field(&mut bytes, object.get(field.name).unwrap(), field)?;
    }
    if schema.stores_hash {
        bytes.extend_from_slice(&hex::decode(hash).unwrap());
    }
    Ok(bytes)
}

fn encode_field(bytes: &mut Vec<u8>, value: &Value, field: &Field) -> Result<(), BrowserCoreError> {
    match field.kind {
        FieldKind::Bool => bytes.push(u8::from(value.as_bool().unwrap())),
        FieldKind::U8 => bytes.push(parse_integer::<u8>(value, field.name)?),
        FieldKind::U16 => {
            bytes.extend_from_slice(&parse_integer::<u16>(value, field.name)?.to_le_bytes())
        }
        FieldKind::U32 => {
            bytes.extend_from_slice(&parse_integer::<u32>(value, field.name)?.to_le_bytes())
        }
        FieldKind::U64 => {
            bytes.extend_from_slice(&parse_integer::<u64>(value, field.name)?.to_le_bytes())
        }
        FieldKind::U128 => {
            bytes.extend_from_slice(&parse_integer::<u128>(value, field.name)?.to_le_bytes())
        }
        FieldKind::I8 => bytes.push(parse_integer::<i8>(value, field.name)? as u8),
        FieldKind::I16 => {
            bytes.extend_from_slice(&parse_integer::<i16>(value, field.name)?.to_le_bytes())
        }
        FieldKind::I32 => {
            bytes.extend_from_slice(&parse_integer::<i32>(value, field.name)?.to_le_bytes())
        }
        FieldKind::I64 => {
            bytes.extend_from_slice(&parse_integer::<i64>(value, field.name)?.to_le_bytes())
        }
        FieldKind::I128 => {
            bytes.extend_from_slice(&parse_integer::<i128>(value, field.name)?.to_le_bytes())
        }
        FieldKind::Address => {
            bytes.extend_from_slice(&address::short_address_to_bytes(value.as_str().unwrap())?)
        }
        FieldKind::VanityAddress => {
            bytes.extend_from_slice(&address::vanity_address_to_bytes(value.as_str().unwrap())?)
        }
        FieldKind::Hash(_) => {
            bytes.extend_from_slice(&hex::decode(value.as_str().unwrap()).unwrap())
        }
        FieldKind::Text(_) | FieldKind::PaddedText(_) => {
            bytes.extend_from_slice(value.as_str().unwrap().as_bytes())
        }
    }
    Ok(())
}

fn field_width(kind: FieldKind) -> usize {
    match kind {
        FieldKind::U8 | FieldKind::I8 | FieldKind::Bool => 1,
        FieldKind::U16 | FieldKind::I16 => 2,
        FieldKind::U32 | FieldKind::I32 => 4,
        FieldKind::U64 | FieldKind::I64 => 8,
        FieldKind::U128 | FieldKind::I128 => 16,
        FieldKind::Address | FieldKind::VanityAddress => 22,
        FieldKind::Hash(length) | FieldKind::Text(length) | FieldKind::PaddedText(length) => length,
    }
}

fn wire_length(schema: &Schema) -> usize {
    let fields: usize = schema
        .fields
        .iter()
        .map(|field| field_width(field.kind))
        .sum();
    fields
        + usize::from(schema.stores_hash) * 32
        + if schema.signatures == SignatureMode::Dual {
            SIGNATURE_LENGTH * 2
        } else {
            SIGNATURE_LENGTH
        }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn registry_contains_every_externally_submitted_type() {
        let types: Vec<u8> = SCHEMAS.iter().map(|schema| schema.txtype).collect();
        assert_eq!(
            types,
            vec![
                2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 100, 101, 102, 103, 104, 105, 106, 107, 108,
                109, 110, 111, 112, 113, 200, 201, 202
            ]
        );
    }

    #[test]
    fn node_only_types_are_not_signable() {
        assert!(matches!(
            schema(0),
            Err(BrowserCoreError::UnsupportedTransaction(0))
        ));
        assert!(matches!(
            schema(1),
            Err(BrowserCoreError::UnsupportedTransaction(1))
        ));
    }

    #[test]
    fn registry_wire_lengths_match_node_command_map() {
        let expected = [
            (2, 750),
            (3, 725),
            (4, 922),
            (5, 861),
            (6, 1471),
            (7, 1492),
            (8, 781),
            (9, 733),
            (10, 728),
            (11, 724),
            (12, 723),
            (100, 701),
            (101, 784),
            (102, 784),
            (103, 785),
            (104, 787),
            (105, 791),
            (106, 799),
            (107, 995),
            (108, 784),
            (109, 785),
            (110, 787),
            (111, 791),
            (112, 799),
            (113, 783),
            (200, 733),
            (201, 734),
            (202, 866),
        ];
        for (txtype, expected_length) in expected {
            assert_eq!(
                wire_length(schema(txtype).unwrap()),
                expected_length,
                "txtype {txtype}"
            );
        }
    }

    #[test]
    fn signed_transaction_bytes_decode_without_losing_fields() {
        let schema = schema(2).unwrap();
        let suffix = crate::address::network_suffix();
        let request = serde_json::json!({
            "txtype": 2,
            "time": 1_700_000_000,
            "value": "9007199254740993",
            "coin": "CLTC",
            "nft_series": 0,
            "sender": format!("1111111111111111111111111111111111111111.{suffix}"),
            "receiver": format!("2222222222222222222222222222222222222222.{suffix}"),
            "txfee": "2500"
        });
        let unsigned = canonical_unsigned(&request, schema).unwrap();
        let signature = "33".repeat(SIGNATURE_LENGTH);
        let bytes = encode_transaction(schema, &unsigned, &"00".repeat(32), Some(&signature), None)
            .unwrap();

        let decoded = decode_transaction(&bytes).unwrap();
        let fields = decoded.fields.as_object().unwrap();
        assert_eq!(decoded.transaction_name, "Transfer");
        assert_eq!(fields["value"], "9007199254740993");
        assert_eq!(fields["coin"], "CLTC");
        assert_eq!(fields["receiver"], format!("2222222222222222222222222222222222222222.{suffix}"));
        assert_eq!(decoded.signature.as_deref(), Some(signature.as_str()));
        assert!(decoded.signature1.is_none());
        assert!(decoded.signature2.is_none());
    }

    #[test]
    fn storage_keys_are_right_padded_during_canonicalization() {
        let schema = schema(101).unwrap();
        let suffix = crate::address::network_suffix();
        let request = serde_json::json!({
            "txtype": 101,
            "time": 1_700_000_000,
            "storagekey": "00".repeat(32),
            "key": "profile_name",
            "value": true,
            "address": format!("1111111111111111111111111111111111111111.{suffix}"),
            "txfee": "100000000"
        });

        let unsigned = canonical_unsigned(&request, schema).unwrap();
        let key = unsigned.get("key").and_then(Value::as_str).unwrap();

        assert_eq!(key.len(), 50);
        assert_eq!(key.trim_end(), "profile_name");
    }

    #[test]
    fn dual_signatures_are_created_independently_and_combined() {
        let first_wallet = crate::wallet::generate("first wallet password").unwrap();
        let second_wallet = crate::wallet::generate("second wallet password").unwrap();
        let first = crate::wallet::unlock(&first_wallet, "first wallet password").unwrap();
        let second = crate::wallet::unlock(&second_wallet, "second wallet password").unwrap();
        let request = serde_json::json!({
            "txtype": 6,
            "timestamp": 1_800_000_000,
            "offer_expiration": 1_800_003_600u32,
            "ticker1": "CLTC",
            "nft_series1": 0,
            "value1": "100000000",
            "ticker2": "TEST",
            "nft_series2": 0,
            "value2": "200000000",
            "sender1": first.short_address,
            "sender2": second.short_address,
            "tip1": "1000000",
            "tip2": "1000000",
            "txfee1": "1000000",
            "txfee2": "1000000"
        });
        let second_private = hex::decode(&second.private_key).unwrap();
        let second_public = hex::decode(&second.public_key).unwrap();
        let first_private = hex::decode(&first.private_key).unwrap();
        let first_public = hex::decode(&first.public_key).unwrap();
        let first_result = sign_request(&request, &first_private, &first_public, 1).unwrap();
        let second_result = sign_request(&request, &second_private, &second_public, 2).unwrap();

        assert!(!first_result.complete);
        assert!(first_result.signature1.is_some());
        assert!(first_result.signature2.is_none());
        assert!(!second_result.complete);
        assert!(second_result.signature1.is_none());
        assert!(second_result.signature2.is_some());
        assert_eq!(first_result.hash, second_result.hash);
        assert_eq!(
            first_result.unsigned_bytes_hex,
            second_result.unsigned_bytes_hex
        );

        let completed = complete_dual_request(
            &request,
            first_result.signature1.as_deref().unwrap(),
            &first_public,
            second_result.signature2.as_deref().unwrap(),
            &second_public,
        )
        .unwrap();

        assert!(completed.complete);
        assert!(completed.signature1.is_some());
        assert!(completed.signature2.is_some());
        assert_eq!(completed.bytes_hex.unwrap().len() / 2, 1471);
    }
}
