#![allow(clippy::type_complexity)]

use {
  self::{command_builder::CommandBuilder, expected::Expected, test_server::TestServer},
  bitcoin::{
    Amount, Network, OutPoint, Psbt, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness,
    address::{Address, NetworkUnchecked},
    blockdata::locktime::absolute::LockTime,
    opcodes, script,
    transaction::Version,
  },
  chrono::{DateTime, Utc},
  mockcore::TransactionTemplate,
  ord::{
    Inscription, InscriptionId, RuneEntry, api, base64_decode, base64_encode, chain::Chain,
    decimal::Decimal, outgoing::Outgoing, subcommand::runes::RuneInfo, templates::InscriptionHtml,
    wallet::ListDescriptorsResult, wallet::batch,
  },
  ordinals::{
    Artifact, COIN_VALUE, Charm, Edict, Pile, Rarity, Rune, RuneId, Runestone, Sat, SatPoint,
    SpacedRune,
  },
  pretty_assertions::assert_eq as pretty_assert_eq,
  regex::Regex,
  reqwest::{StatusCode, Url},
  serde::de::DeserializeOwned,
  std::sync::Arc,
  std::{
    collections::{BTreeMap, BTreeSet},
    ffi::{OsStr, OsString},
    fs,
    io::{BufRead, BufReader, Write},
    net::TcpListener,
    path::{Path, PathBuf},
    process::{Child, Command, Stdio},
    str::{self, FromStr},
    thread,
    time::Duration,
  },
  tempfile::TempDir,
};

macro_rules! assert_regex_match {
  ($value:expr, $pattern:expr $(,)?) => {
    let regex = Regex::new(&format!("^(?s){}$", $pattern)).unwrap();
    let string = $value.to_string();

    if !regex.is_match(string.as_ref()) {
      eprintln!("Regex did not match:");
      pretty_assert_eq!(regex.as_str(), string);
    }
  };
}

mod command_builder;
mod expected;
mod test_server;

mod balances;
mod decode;
mod epochs;
mod find;
mod index;
mod info;
mod json_api;
mod list;
mod parse;
mod runes;
mod server;
mod settings;
mod subsidy;
mod supply;
mod traits;
mod verify;
mod version;
mod wallet;

const RUNE: u128 = 99246114928149462;

type Balance = ord::subcommand::wallet::balance::Output;
type Balances = ord::subcommand::balances::Output;
type Batch = ord::wallet::batch::Output;
type Create = ord::subcommand::wallet::create::Output;
type Inscriptions = Vec<ord::subcommand::wallet::inscriptions::Output>;
type Send = ord::subcommand::wallet::send::Output;
type Split = ord::subcommand::wallet::split::Output;
type Supply = ord::subcommand::supply::Output;
type Sweep = ord::subcommand::wallet::sweep::Output;

fn create_wallet(core: &mockcore::Handle, ord: &TestServer) {
  CommandBuilder::new(format!("--chain {} wallet create", core.network()))
    .core(core)
    .ord(ord)
    .stdout_regex(".*")
    .run_and_extract_stdout();
}

fn sats(
  core: &mockcore::Handle,
  ord: &TestServer,
) -> Vec<ord::subcommand::wallet::sats::OutputRare> {
  CommandBuilder::new(format!("--chain {} wallet sats", core.network()))
    .core(core)
    .ord(ord)
    .run_and_deserialize_output::<Vec<ord::subcommand::wallet::sats::OutputRare>>()
}

fn inscribe_with_options(
  core: &mockcore::Handle,
  ord: &TestServer,
  postage: Option<u64>,
  fee_rate: u64,
) -> (InscriptionId, Txid) {
  core.mine_blocks(1);

  let mut command_str = format!(
    "--chain {} wallet inscribe --fee-rate {} --file foo.txt",
    core.network(),
    fee_rate,
  );

  if let Some(postage_value) = postage {
    command_str.push_str(&format!(" --postage {postage_value}sat"));
  }

  let output = CommandBuilder::new(command_str)
    .write("foo.txt", "FOO")
    .core(core)
    .ord(ord)
    .run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  assert_eq!(output.inscriptions.len(), 1);

  (output.inscriptions[0].id, output.reveal)
}

fn inscribe(core: &mockcore::Handle, ord: &TestServer) -> (InscriptionId, Txid) {
  inscribe_with_options(core, ord, None, 1)
}

fn drain(core: &mockcore::Handle, ord: &TestServer) {
  let balance = CommandBuilder::new("--regtest --index-runes wallet balance")
    .core(core)
    .ord(ord)
    .run_and_deserialize_output::<Balance>();

  CommandBuilder::new(format!(
    "
      --chain regtest
      --index-runes
      wallet send
      --fee-rate 0
      bcrt1pyrmadgg78e38ewfv0an8c6eppk2fttv5vnuvz04yza60qau5va0saknu8k
      {}sat
    ",
    balance.cardinal
  ))
  .core(core)
  .ord(ord)
  .run_and_deserialize_output::<Send>();

  core.mine_blocks_with_subsidy(1, 0);

  let balance = CommandBuilder::new("--regtest --index-runes wallet balance")
    .core(core)
    .ord(ord)
    .run_and_deserialize_output::<Balance>();

  pretty_assert_eq!(balance.cardinal, 0);
}

struct Etched {
  id: RuneId,
  output: Batch,
}

fn etch(core: &mockcore::Handle, ord: &TestServer, rune: Rune) -> Etched {
  batch(
    core,
    ord,
    batch::File {
      etching: Some(batch::Etching {
        supply: "1000".parse().unwrap(),
        divisibility: 0,
        terms: None,
        premine: "1000".parse().unwrap(),
        rune: SpacedRune { rune, spacers: 0 },
        symbol: '¢',
        turbo: false,
      }),
      inscriptions: vec![batch::Entry {
        file: Some("inscription.jpeg".into()),
        ..default()
      }],
      ..default()
    },
  )
}

fn batch(core: &mockcore::Handle, ord: &TestServer, batchfile: batch::File) -> Etched {
  core.mine_blocks(1);

  let mut builder =
    CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml")
      .write("batch.yaml", serde_yaml::to_string(&batchfile).unwrap())
      .core(core)
      .ord(ord);

  for inscription in &batchfile.inscriptions {
    builder = builder.write(inscription.file.clone().unwrap(), "inscription");
  }

  let mut spawn = builder.spawn();

  let mut buffer = String::new();

  BufReader::new(spawn.child.stderr.as_mut().unwrap())
    .read_line(&mut buffer)
    .unwrap();

  assert_regex_match!(
    buffer,
    "Waiting for rune .* commitment [[:xdigit:]]{64} to mature…\n"
  );

  core.mine_blocks(5);

  let output = spawn.run_and_deserialize_output::<Batch>();

  core.mine_blocks(1);

  let block_height = core.height();

  let id = RuneId {
    block: block_height,
    tx: 1,
  };

  let reveal = output.reveal;
  let parent = output.inscriptions[0].id;

  let batch::Etching {
    divisibility,
    premine,
    rune,
    supply,
    symbol,
    terms,
    turbo,
  } = batchfile.etching.unwrap();

  {
    let supply = supply.to_integer(divisibility).unwrap();
    let premine = premine.to_integer(divisibility).unwrap();

    let mintable = terms
      .map(|terms| terms.cap * terms.amount.to_integer(divisibility).unwrap())
      .unwrap_or_default();

    assert_eq!(supply, premine + mintable);
  }

  let mut mint_definition = Vec::<String>::new();

  if let Some(terms) = terms {
    mint_definition.push("<dd>".into());
    mint_definition.push("<dl>".into());

    let mut mintable = true;

    mint_definition.push("<dt>start</dt>".into());
    {
      let relative = terms
        .offset
        .and_then(|range| range.start)
        .map(|start| start + block_height);
      let absolute = terms.height.and_then(|range| range.start);

      let start = relative
        .zip(absolute)
        .map(|(relative, absolute)| relative.max(absolute))
        .or(relative)
        .or(absolute);

      if let Some(start) = start {
        mintable &= block_height + 1 >= start;
        mint_definition.push(format!("<dd><a href=/block/{start}>{start}</a></dd>"));
      } else {
        mint_definition.push("<dd>none</dd>".into());
      }
    }

    mint_definition.push("<dt>end</dt>".into());
    {
      let relative = terms
        .offset
        .and_then(|range| range.end)
        .map(|end| end + block_height);
      let absolute = terms.height.and_then(|range| range.end);

      let end = relative
        .zip(absolute)
        .map(|(relative, absolute)| relative.min(absolute))
        .or(relative)
        .or(absolute);

      if let Some(end) = end {
        mintable &= block_height + 1 < end;
        mint_definition.push(format!("<dd><a href=/block/{end}>{end}</a></dd>"));
      } else {
        mint_definition.push("<dd>none</dd>".into());
      }
    }

    mint_definition.push("<dt>amount</dt>".into());

    mint_definition.push(format!(
      "<dd>{}</dd>",
      Pile {
        amount: terms.amount.to_integer(divisibility).unwrap(),
        divisibility,
        symbol: Some(symbol),
      }
    ));

    mint_definition.push("<dt>mints</dt>".into());
    mint_definition.push("<dd>0</dd>".into());
    mint_definition.push("<dt>cap</dt>".into());
    mint_definition.push(format!("<dd>{}</dd>", terms.cap));
    mint_definition.push("<dt>remaining</dt>".into());
    mint_definition.push(format!("<dd>{}</dd>", terms.cap));

    mint_definition.push("<dt>mintable</dt>".into());
    mint_definition.push(format!("<dd>{mintable}</dd>"));

    if mintable {
      mint_definition.push("<dt>progress</dt>".into());
      mint_definition.push("<dd>0%</dd>".into());
    }

    mint_definition.push("</dl>".into());
    mint_definition.push("</dd>".into());
  } else {
    mint_definition.push("<dd>no</dd>".into());
  }

  let RuneId { block, tx } = id;

  ord.assert_response_regex(
    format!("/rune/{rune}"),
    format!(
      r".*<dt>id</dt>
  <dd>{id}</dd>.*
  <dt>etching block</dt>
  <dd><a href=/block/{block}>{block}</a></dd>
  <dt>etching transaction</dt>
  <dd>{tx}</dd>
  <dt>mint</dt>
  {}
  <dt>supply</dt>
  <dd>{premine} {symbol}</dd>
  <dt>premine</dt>
  <dd>{premine} {symbol}</dd>
  <dt>premine percentage</dt>
  <dd>.*</dd>
  <dt>burned</dt>
  <dd>0 {symbol}</dd>
  <dt>divisibility</dt>
  <dd>{divisibility}</dd>
  <dt>symbol</dt>
  <dd>{symbol}</dd>
  <dt>turbo</dt>
  <dd>{turbo}</dd>
  <dt>etching</dt>
  <dd><a class=collapse href=/tx/{reveal}>{reveal}</a></dd>
  <dt>parent</dt>
  <dd><a class=collapse href=/inscription/{parent}>{parent}</a></dd>
.*",
      mint_definition.join("\\s+"),
    ),
  );

  let batch::RuneInfo {
    destination,
    location,
    rune: _,
  } = output.rune.clone().unwrap();

  if premine.to_integer(divisibility).unwrap() > 0 {
    let destination = destination
      .unwrap()
      .clone()
      .require_network(Network::Regtest)
      .unwrap();

    assert!(core.state().is_wallet_address(&destination));

    let location = location.unwrap();

    assert_eq!(core.address(location), destination);
  } else {
    assert!(destination.is_none());
    assert!(location.is_none());
  }

  let response = ord.json_request("/inscriptions");

  assert!(response.status().is_success());

  for id in response.json::<api::Inscriptions>().unwrap().ids {
    let response = ord.json_request(format!("/inscription/{id}"));
    assert!(response.status().is_success());
    if let Some(location) = location {
      let inscription = response.json::<api::Inscription>().unwrap();
      assert!(inscription.satpoint.outpoint != location);
    }
  }

  Etched { output, id }
}

fn envelope(payload: &[&[u8]]) -> Witness {
  let mut builder = bitcoin::script::Builder::new()
    .push_opcode(bitcoin::opcodes::OP_FALSE)
    .push_opcode(bitcoin::opcodes::all::OP_IF);

  for data in payload {
    let mut buf = bitcoin::script::PushBytesBuf::new();
    buf.extend_from_slice(data).unwrap();
    builder = builder.push_slice(buf);
  }

  let script = builder
    .push_opcode(bitcoin::opcodes::all::OP_ENDIF)
    .into_script();

  Witness::from_slice(&[script.into_bytes(), Vec::new()])
}

fn default<T: Default>() -> T {
  Default::default()
}
