use super::*;

#[serde_with::skip_serializing_none]
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Default)]
#[serde(deny_unknown_fields)]
pub struct File {
  pub mode: Mode,
  #[serde(default)]
  pub parents: Vec<InscriptionId>,
  pub postage: Option<u64>,
  #[serde(default)]
  pub reinscribe: bool,
  pub sat: Option<Sat>,
  pub satpoint: Option<SatPoint>,
  pub inscriptions: Vec<batch::Entry>,
  pub etching: Option<batch::Etching>,
}

impl File {
  pub(crate) fn load(path: &Path) -> Result<Self> {
    let batchfile: Self = serde_yaml::from_reader(fs::File::open(path)?)?;

    ensure!(
      !batchfile.inscriptions.is_empty(),
      "batchfile must contain at least one inscription",
    );

    let sat_and_satpoint = batchfile.sat.is_some() && batchfile.satpoint.is_some();

    ensure!(
      !sat_and_satpoint,
      "batchfile cannot set both `sat` and `satpoint`",
    );

    let sat_or_satpoint = batchfile.sat.is_some() || batchfile.satpoint.is_some();

    if sat_or_satpoint {
      ensure!(
        batchfile.mode == Mode::SameSat,
        "`sat` or `satpoint` can only be set in `same-sat` mode",
      );
    }

    if batchfile
      .inscriptions
      .iter()
      .any(|entry| entry.destination.is_some())
      && (batchfile.mode == Mode::SharedOutput || batchfile.mode == Mode::SameSat)
    {
      bail!(
        "individual inscription destinations cannot be set in `shared-output` or `same-sat` mode"
      );
    }

    for inscription in &batchfile.inscriptions {
      let mut items = BTreeSet::new();

      for item in &inscription.gallery {
        ensure! {
          items.insert(item.id),
          "duplicate gallery item: {}",
          item.id,
        }
      }
    }

    let any_entry_has_satpoint = batchfile
      .inscriptions
      .iter()
      .any(|entry| entry.satpoint.is_some());

    if any_entry_has_satpoint {
      ensure!(
        batchfile.mode == Mode::SatPoints,
        "specifying `satpoint` in an inscription only works in `satpoints` mode"
      );

      ensure!(
        batchfile
          .inscriptions
          .iter()
          .all(|entry| entry.satpoint.is_some()),
        "if `satpoint` is set for any inscription, then all inscriptions need to specify a satpoint"
      );

      ensure!(
        batchfile
          .inscriptions
          .iter()
          .all(|entry| entry.satpoint.unwrap().offset == 0),
        "`satpoint` can only be specified for first sat of an output"
      );
    }

    if batchfile.mode == Mode::SatPoints {
      ensure!(
        batchfile.postage.is_none(),
        "`postage` cannot be set if in `satpoints` mode"
      );

      ensure!(
        batchfile.sat.is_none(),
        "`sat` cannot be set if in `satpoints` mode"
      );

      ensure!(
        batchfile.satpoint.is_none(),
        "`satpoint cannot be set if in `satpoints` mode"
      );

      let mut seen = HashSet::new();
      for entry in batchfile.inscriptions.iter() {
        let satpoint = entry.satpoint.unwrap_or_default();
        if !seen.insert(satpoint) {
          bail!("duplicate satpoint {}", satpoint);
        }
      }
    }

    Ok(batchfile)
  }

  pub(crate) fn inscriptions(
    &self,
    wallet: &Wallet,
    utxos: &BTreeMap<OutPoint, TxOut>,
    parent_values: Vec<u64>,
    compress: bool,
  ) -> Result<(
    Vec<Inscription>,
    Vec<(SatPoint, TxOut)>,
    Vec<Amount>,
    Vec<Address>,
  )> {
    let mut inscriptions = Vec::new();
    let mut reveal_satpoints = Vec::new();
    let mut postages = Vec::new();

    let mut pointer = parent_values.iter().sum();

    for (i, entry) in self.inscriptions.iter().enumerate() {
      inscriptions.push(Inscription::new(
        wallet.chain(),
        compress,
        entry.delegate,
        entry.metadata()?,
        entry.metaprotocol.clone(),
        self.parents.clone(),
        entry.file.clone(),
        Some(pointer),
        Properties {
          gallery: entry
            .gallery
            .clone()
            .into_iter()
            .map(|item| Item {
              id: Some(item.id),
              attributes: item.attributes,
              index: None,
            })
            .collect(),
          attributes: entry.attributes.clone(),
          txids: Vec::new(),
        },
        self
          .etching
          .and_then(|etch| (i == 0).then_some(etch.rune.rune)),
      )?);

      let postage = if self.mode == Mode::SatPoints {
        let satpoint = entry
          .satpoint
          .ok_or_else(|| anyhow!("no satpoint specified for entry {i}"))?;

        let txout = utxos
          .get(&satpoint.outpoint)
          .ok_or_else(|| anyhow!("{} not in wallet", satpoint))?;

        reveal_satpoints.push((satpoint, txout.clone()));

        txout.value
      } else {
        self.postage.map(Amount::from_sat).unwrap_or(TARGET_POSTAGE)
      };

      if self.mode != Mode::SameSat {
        pointer += postage.to_sat();
      }

      if self.mode == Mode::SameSat && i > 0 {
        continue;
      } else {
        postages.push(postage);
      }
    }

    let destinations = match self.mode {
      Mode::SharedOutput | Mode::SameSat => vec![wallet.get_change_address()?],
      Mode::SeparateOutputs | Mode::SatPoints => self
        .inscriptions
        .iter()
        .map(|entry| {
          entry.destination.as_ref().map_or_else(
            || wallet.get_change_address(),
            |address| {
              address
                .clone()
                .require_network(wallet.chain().network())
                .map_err(|e| e.into())
            },
          )
        })
        .collect::<Result<Vec<_>, _>>()?,
    };

    Ok((inscriptions, reveal_satpoints, postages, destinations))
  }
}

#[cfg(test)]
mod tests {
  use {super::*, pretty_assertions::assert_eq};

  #[test]
  fn batchfile_not_sat_and_satpoint() {
    let tempdir = TempDir::new().unwrap();
    let batch_file = tempdir.path().join("batch.yaml");
    fs::write(
      batch_file.clone(),
      r#"
mode: same-sat
sat: 55555
satpoint: 4651dc5e964879b1eb9183d467d1187dcd252504698002b01853446c460db2c5:0:0
inscriptions:
- file: inscription.txt
- file: tulip.png
- file: meow.wav
"#,
    )
    .unwrap();

    assert_eq!(
      File::load(batch_file.as_path()).unwrap_err().to_string(),
      "batchfile cannot set both `sat` and `satpoint`"
    );
  }

  #[test]
  fn batchfile_wrong_mode_for_satpoints() {
    let tempdir = TempDir::new().unwrap();
    let batch_file = tempdir.path().join("batch.yaml");
    fs::write(
      batch_file.clone(),
      r#"
mode: separate-outputs
inscriptions:
- file: inscription.txt
  satpoint: bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:0:0
- file: tulip.png
  satpoint: 5fddcbdc3eb21a93e8dd1dd3f9087c3677f422b82d5ba39a6b1ec37338154af6:0:0
- file: meow.wav
  satpoint: 4651dc5e964879b1eb9183d467d1187dcd252504698002b01853446c460db2c5:0:0
"#,
    )
    .unwrap();

    assert_eq!(
      batch::File::load(batch_file.as_path())
        .unwrap_err()
        .to_string(),
      "specifying `satpoint` in an inscription only works in `satpoints` mode"
    );
  }

  #[test]
  fn batchfile_missing_satpoint() {
    let tempdir = TempDir::new().unwrap();
    let batch_file = tempdir.path().join("batch.yaml");
    fs::write(
      batch_file.clone(),
      r#"
mode: satpoints
inscriptions:
- file: inscription.txt
  satpoint: bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:0:0
- file: tulip.png
- file: meow.wav
  satpoint: 4651dc5e964879b1eb9183d467d1187dcd252504698002b01853446c460db2c5:0:0
"#,
    )
    .unwrap();

    assert_eq!(
      batch::File::load(batch_file.as_path())
        .unwrap_err()
        .to_string(),
      "if `satpoint` is set for any inscription, then all inscriptions need to specify a satpoint"
    );
  }

  #[test]
  fn batchfile_only_first_sat_of_outpoint() {
    let tempdir = TempDir::new().unwrap();
    let batch_file = tempdir.path().join("batch.yaml");
    fs::write(
      batch_file.clone(),
      r#"
mode: satpoints
inscriptions:
- file: inscription.txt
  satpoint: bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:0:0
- file: tulip.png
  satpoint: 5fddcbdc3eb21a93e8dd1dd3f9087c3677f422b82d5ba39a6b1ec37338154af6:0:21
- file: meow.wav
  satpoint: 4651dc5e964879b1eb9183d467d1187dcd252504698002b01853446c460db2c5:0:0
"#,
    )
    .unwrap();

    assert_eq!(
      batch::File::load(batch_file.as_path())
        .unwrap_err()
        .to_string(),
      "`satpoint` can only be specified for first sat of an output"
    );
  }

  #[test]
  fn batchfile_no_postage_if_mode_satpoints() {
    let tempdir = TempDir::new().unwrap();
    let batch_file = tempdir.path().join("batch.yaml");
    fs::write(
      batch_file.clone(),
      r#"
mode: satpoints
postage: 1111
inscriptions:
- file: inscription.txt
  satpoint: bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:0:0
- file: tulip.png
  satpoint: 5fddcbdc3eb21a93e8dd1dd3f9087c3677f422b82d5ba39a6b1ec37338154af6:0:0
- file: meow.wav
  satpoint: 4651dc5e964879b1eb9183d467d1187dcd252504698002b01853446c460db2c5:0:0
"#,
    )
    .unwrap();

    assert_eq!(
      batch::File::load(batch_file.as_path())
        .unwrap_err()
        .to_string(),
      "`postage` cannot be set if in `satpoints` mode"
    );
  }

  #[test]
  fn batchfile_no_duplicate_satpoints() {
    let tempdir = TempDir::new().unwrap();
    let batch_file = tempdir.path().join("batch.yaml");
    fs::write(
      batch_file.clone(),
      r#"
mode: satpoints
inscriptions:
- file: inscription.txt
  satpoint: bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:0:0
- file: tulip.png
  satpoint: 5fddcbdc3eb21a93e8dd1dd3f9087c3677f422b82d5ba39a6b1ec37338154af6:0:0
- file: meow.wav
  satpoint: 4651dc5e964879b1eb9183d467d1187dcd252504698002b01853446c460db2c5:0:0
- file: inscription_1.txt
  satpoint: bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:0:0
"#,
    )
    .unwrap();

    assert_eq!(
      batch::File::load(batch_file.as_path())
        .unwrap_err()
        .to_string(),
      "duplicate satpoint bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:0:0"
    );
  }

  #[test]
  fn example_batchfile_deserializes_successfully() {
    assert_eq!(
      batch::File::load(Path::new("batch.yaml")).unwrap(),
      batch::File {
        mode: batch::Mode::SeparateOutputs,
        parents: vec![
          "6ac5cacb768794f4fd7a78bf00f2074891fce68bd65c4ff36e77177237aacacai0"
            .parse()
            .unwrap()
        ],
        postage: Some(12345),
        reinscribe: true,
        sat: None,
        satpoint: None,
        etching: Some(Etching {
          rune: "THE•BEST•RUNE".parse().unwrap(),
          divisibility: 2,
          premine: "1000.00".parse().unwrap(),
          supply: "10000.00".parse().unwrap(),
          symbol: '$',
          terms: Some(batch::Terms {
            amount: "100.00".parse().unwrap(),
            cap: 90,
            height: Some(batch::Range {
              start: Some(840000),
              end: Some(850000),
            }),
            offset: Some(batch::Range {
              start: Some(1000),
              end: Some(9000),
            }),
          }),
          turbo: true,
        }),
        inscriptions: vec![
          batch::Entry {
            file: Some("mango.avif".into()),
            delegate: Some(
              "6ac5cacb768794f4fd7a78bf00f2074891fce68bd65c4ff36e77177237aacacai0"
                .parse()
                .unwrap()
            ),
            destination: Some(
              "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"
                .parse()
                .unwrap()
            ),
            metadata: Some(serde_yaml::Value::Mapping({
              let mut mapping = serde_yaml::Mapping::new();
              mapping.insert("title".into(), "Delicious Mangos".into());
              mapping.insert(
                "description".into(),
                "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam semper, \
                ligula ornare laoreet tincidunt, odio nisi euismod tortor, vel blandit \
                metus est et odio. Nullam venenatis, urna et molestie vestibulum, orci \
                mi efficitur risus, eu malesuada diam lorem sed velit. Nam fermentum \
                dolor et luctus euismod.\n"
                  .into(),
              );
              mapping
            })),
            attributes: Attributes {
              title: Some("Delicious Mangos".into()),
              traits: Traits {
                items: vec![
                  ("color".into(), Trait::String("orange".into())),
                  ("deliciousness".into(), Trait::Integer(1000)),
                ],
              },
            },
            ..default()
          },
          batch::Entry {
            file: Some("token.json".into()),
            metaprotocol: Some("DOPEPROTOCOL-42069".into()),
            ..default()
          },
          batch::Entry {
            file: Some("tulip.png".into()),
            destination: Some(
              "bc1pdqrcrxa8vx6gy75mfdfj84puhxffh4fq46h3gkp6jxdd0vjcsdyspfxcv6"
                .parse()
                .unwrap()
            ),
            metadata: Some(serde_yaml::Value::Mapping({
              let mut mapping = serde_yaml::Mapping::new();
              mapping.insert("author".into(), "Satoshi Nakamoto".into());
              mapping
            })),
            ..default()
          },
          batch::Entry {
            file: Some("gallery.png".into()),
            gallery: vec![
              batch::entry::Item {
                id: "a4676e57277b70171d69dc6ad2781485b491fe0ff5870f6f6b01999e7180b29ei0"
                  .parse()
                  .unwrap(),
                attributes: Attributes {
                  title: Some("Incredible".into()),
                  traits: Traits {
                    items: vec![
                      ("background".into(), Trait::String("blue".into())),
                      ("cool".into(), Trait::Bool(true)),
                    ],
                  },
                },
              },
              batch::entry::Item {
                id: "a4676e57277b70171d69dc6ad2781485b491fe0ff5870f6f6b01999e7180b29ei3"
                  .parse()
                  .unwrap(),
                attributes: Attributes {
                  title: None,
                  traits: Traits::default(),
                },
              },
            ],
            ..default()
          },
        ],
      }
    );
  }

  #[test]
  fn batchfile_no_delegate_no_file_allowed() {
    let tempdir = TempDir::new().unwrap();
    let batch_file = tempdir.path().join("batch.yaml");
    fs::write(
      batch_file.clone(),
      r#"
mode: shared-output
inscriptions:
  -
"#,
    )
    .unwrap();

    assert!(batch::File::load(batch_file.as_path()).is_ok());
  }

  #[test]
  fn batchfile_no_duplicate_gallery_items() {
    let tempdir = TempDir::new().unwrap();
    let batch_file = tempdir.path().join("batch.yaml");
    fs::write(
      batch_file.clone(),
      r#"
mode: separate-outputs
inscriptions:
- file: inscription.txt
  gallery:
  - id: 6ac5cacb768794f4fd7a78bf00f2074891fce68bd65c4ff36e77177237aacacai0
  - id: 6ac5cacb768794f4fd7a78bf00f2074891fce68bd65c4ff36e77177237aacacai0
"#,
    )
    .unwrap();

    assert_eq!(
      batch::File::load(batch_file.as_path())
        .unwrap_err()
        .to_string(),
      "duplicate gallery item: 6ac5cacb768794f4fd7a78bf00f2074891fce68bd65c4ff36e77177237aacacai0"
    );
  }
}
