use std::collections::BTreeMap;
use std::convert::TryInto as _;
use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Result as FmtResult;
use std::fs::create_dir_all;
use std::fs::OpenOptions;
use std::io::Write as _;
use std::mem::size_of;
use std::ops::Deref as _;
use std::path::PathBuf;
use std::slice::from_ref as slice_from_ref;
use std::str::from_utf8 as str_from_utf8;
use anyhow::ensure;
use anyhow::Context as _;
use anyhow::Result;
use sha2::Digest as _;
use sha2::Sha256;
use serde::Deserialize;
use serde::Serialize;
use serde_json::from_slice;
use serde_json::to_writer;
use tracing::warn;
use warp::hyper::body::Bytes;
use crate::index::Entry;
use crate::index::Index;
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
enum Kind {
  Dev,
  Build,
  Normal,
}
impl Display for Kind {
  fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
    let s = match self {
      Kind::Dev => "dev",
      Kind::Build => "build",
      Kind::Normal => "normal",
    };
    f.write_str(s)
  }
}
#[derive(Debug, Deserialize)]
struct Dep {
  name: String,
  version_req: String,
  features: Vec<String>,
  optional: bool,
  default_features: bool,
  target: Option<String>,
  kind: Kind,
  registry: Option<String>,
  explicit_name_in_toml: Option<String>,
}
impl From<Dep> for crate::index::Dep {
  fn from(source: Dep) -> Self {
    Self {
      name: source.name,
      req: source.version_req,
      features: source.features,
      optional: source.optional,
      default_features: source.default_features,
      target: source.target,
      kind: source.kind.to_string(),
      registry: source.registry,
      package: source.explicit_name_in_toml,
    }
  }
}
#[derive(Debug, Deserialize)]
#[allow(unused)]
struct MetaData {
  name: String,
  vers: String,
  deps: Vec<Dep>,
  features: BTreeMap<String, Vec<String>>,
  authors: Vec<String>,
  description: Option<String>,
  documentation: Option<String>,
  homepage: Option<String>,
  readme: Option<String>,
  readme_file: Option<String>,
  keywords: Vec<String>,
  categories: Vec<String>,
  license: Option<String>,
  license_file: Option<String>,
  repository: Option<String>,
  badges: BTreeMap<String, BTreeMap<String, String>>,
  links: Option<String>,
}
impl From<(MetaData, &[u8])> for Entry {
  fn from(source: (MetaData, &[u8])) -> Self {
    let (metadata, data) = source;
    Self {
      name: metadata.name,
      vers: metadata.vers,
      deps: metadata
        .deps
        .into_iter()
        .map(crate::index::Dep::from)
        .collect(),
      cksum: format!("{:x}", Sha256::digest(data)),
      features: metadata.features,
      yanked: false,
      links: metadata.links,
    }
  }
}
pub fn crate_file_name(name: &str, version: &str) -> String {
  format!("{}-{}.crate", name, version)
}
fn parse_u32(bytes: &mut Bytes) -> Result<u32> {
  ensure!(bytes.len() >= size_of::<u32>(), "not enough data for u32");
  let value = bytes.split_to(size_of::<u32>());
  let value = u32::from_ne_bytes(value.deref().try_into().unwrap());
  Ok(value)
}
fn parse_metadata(bytes: &mut Bytes, json_length: usize) -> Result<MetaData> {
  ensure!(bytes.len() >= json_length, "insufficient data in body");
  let json_body = bytes.split_to(json_length);
  let metadata = from_slice::<MetaData>(&json_body).context("failed to parse JSON metadata")?;
  Ok(metadata)
}
fn crate_path(name: &str) -> PathBuf {
  debug_assert!(name.is_ascii());
  fn to_str(c: &u8) -> &str {
    str_from_utf8(slice_from_ref(c)).unwrap()
  }
  match name.as_bytes() {
    [] => unreachable!(),
    [_] => PathBuf::from("1"),
    [_, _] => PathBuf::from("2"),
    [c, _, _] => ["3", to_str(c)].iter().collect(),
    [c1, c2, c3, c4, ..] => [
      format!("{}{}", to_str(c1), to_str(c2)),
      format!("{}{}", to_str(c3), to_str(c4)),
    ]
    .iter()
    .collect(),
  }
}
fn read_crate(bytes: &mut Bytes, crate_length: usize) -> Result<Bytes> {
  ensure!(bytes.len() >= crate_length, "not enough data for crate");
  let data = bytes.split_to(crate_length);
  Ok(data)
}
pub fn publish_crate(mut body: Bytes, index: &mut Index) -> Result<()> {
  let json_length = parse_u32(&mut body)
    .context("failed to read JSON length")?
    .try_into()
    .unwrap();
  let metadata = parse_metadata(&mut body, json_length).context("failed to read JSON body")?;
  let crate_name = metadata.name.clone();
  let crate_vers = metadata.vers.clone();
  ensure!(!crate_name.is_empty(), "crate name cannot be empty");
  ensure!(
    crate_name.is_ascii(),
    "crate name contains non-ASCII characters"
  );
  let crate_meta_dir = index.root().join(crate_path(&crate_name));
  create_dir_all(&crate_meta_dir)
    .with_context(|| format!("failed to create directory {}", crate_meta_dir.display()))?;
  let crate_length = parse_u32(&mut body)
    .context("failed to read crate length")?
    .try_into()
    .unwrap();
  let data = read_crate(&mut body, crate_length).context("failed to read crate data")?;
  let crate_meta_path = crate_meta_dir.join(&crate_name);
  let mut file = OpenOptions::new()
    .write(true)
    .create(true)
    .append(true)
    .open(&crate_meta_path)
    .with_context(|| {
      format!(
        "failed to create crate index file {}",
        crate_meta_path.display()
      )
    })?;
  let entry = Entry::from((metadata, data.deref()));
  to_writer(&mut file, &entry).context("failed to write crate index meta data")?;
  writeln!(file).context("failed to append new line to crate index meta data file")?;
  let crate_file_name = crate_file_name(&crate_name, &crate_vers);
  let crate_path = index.root().join(crate_file_name);
  let mut file = OpenOptions::new()
    .write(true)
    .create(true)
    .truncate(true)
    .open(&crate_path)
    .with_context(|| format!("failed to create crate file {}", crate_path.display()))?;
  file
    .write(&data)
    .with_context(|| format!("failed to write to crate file {}", crate_path.display()))?;
  index.add(&crate_meta_path).with_context(|| {
    format!(
      "failed to add {} to git repository",
      crate_meta_path.display()
    )
  })?;
  index
    .add(&crate_path)
    .with_context(|| format!("failed to add {} to git repository", crate_path.display()))?;
  index
    .commit(&format!("Add {} in version {}", crate_name, crate_vers))
    .context("failed to commit changes to index")?;
  if !body.is_empty() {
    warn!("body has {} bytes left", body.len());
  }
  Ok(())
}
#[cfg(test)]
mod tests {
  use super::*;
  use std::path::Path;
  #[test]
  fn parse_short_length() {
    let mut body = Bytes::from([255u8, 255, 255].as_ref());
    let err = parse_u32(&mut body).unwrap_err();
    assert_eq!(err.to_string(), "not enough data for u32");
  }
  #[test]
  fn parse_exact_length() {
    let mut body = Bytes::from([44u8, 1, 0, 0].as_ref());
    let length = parse_u32(&mut body).unwrap();
    assert_eq!(length, 300);
    assert!(body.is_empty());
  }
  #[test]
  fn parse_longer_length() {
    let mut body = Bytes::from([142u8, 3, 0, 0, 123].as_ref());
    let length = parse_u32(&mut body).unwrap();
    assert_eq!(length, 910);
    assert_eq!(body.len(), 1);
  }
  #[test]
  fn crate_path_construction() {
    assert_eq!(&crate_path("r"), Path::new("1"));
    assert_eq!(&crate_path("xy"), Path::new("2"));
    assert_eq!(&crate_path("abc"), Path::new("3/a"));
    assert_eq!(&crate_path("abcd"), Path::new("ab/cd"));
    assert_eq!(&crate_path("ydasdayusiy"), Path::new("yd/as"));
  }
}