geolite 0.0.1

open source geocode solution
use std::{fs, path::PathBuf};

use serde::Deserialize;

use crate::database;

#[derive(Deserialize)]
struct geofabrik_index {
  features: Vec<geofabrik_feature>,
}

#[derive(Deserialize)]
struct geofabrik_feature {
  properties: geofabrik_properties,
}

#[derive(Deserialize)]
struct geofabrik_properties {
  id: String,
  name: String,
  #[serde(default)]
  parent: Option<String>,
  #[serde(default)]
  urls: Option<geofabrik_urls>,
}

#[derive(Deserialize)]
struct geofabrik_urls {
  pbf: Option<String>,
}

pub struct ls_output_item {
  pub id: String,
  pub name: String,
  pub url: String,
}

pub struct local_pbf {
  pub path: PathBuf,
  pub size_bytes: u64,
}

pub fn geofabrik(sqlite_path: &str, recreate_cache: bool, endpoint: &str) -> Vec<ls_output_item> {
  let conn = database::open_write(sqlite_path);
  if !recreate_cache {
    let cached = database::osm_pbf_files::list_geofabrik_index(&conn);
    if !cached.is_empty() {
      return cached
        .into_iter()
        .map(|(id, name, url)| ls_output_item { id, name, url })
        .collect();
    }
  }
  let body = super::agent()
    .get(endpoint)
    .call()
    .expect("failed to fetch geofabrik index")
    .into_body()
    .read_to_string()
    .expect("failed to read response body");
  let index: geofabrik_index =
    serde_json::from_str(&body).expect("failed to parse geofabrik index");
  let tx = conn
    .unchecked_transaction()
    .expect("failed to begin transaction");
  for f in &index.features {
    let url = f
      .properties
      .urls
      .as_ref()
      .and_then(|u| u.pbf.as_deref())
      .unwrap_or("-");
    database::osm_pbf_files::upsert_geofabrik_index_item(
      &tx,
      &f.properties.id,
      &f.properties.name,
      f.properties.parent.as_deref(),
      url,
    );
  }
  tx.commit().expect("failed to commit transaction");
  database::osm_pbf_files::list_geofabrik_index(&conn)
    .into_iter()
    .map(|(id, name, url)| ls_output_item { id, name, url })
    .collect()
}

pub fn resolve_geofabrik_url(sqlite_path: &str, id: &str, endpoint: &str) -> Option<String> {
  let conn = database::open_write(sqlite_path);
  if let Some(url) = database::osm_pbf_files::get_geofabrik_url(&conn, id)
    && url != "-"
  {
    return Some(url);
  }
  drop(conn);
  geofabrik(sqlite_path, false, endpoint)
    .into_iter()
    .find(|i| i.id == id)
    .and_then(|i| if i.url == "-" { None } else { Some(i.url) })
}

pub fn list_local(data_path: &str) -> Vec<local_pbf> {
  let pbf_dir = PathBuf::from(data_path);
  let Ok(entries) = fs::read_dir(&pbf_dir) else {
    return vec![];
  };
  let mut result: Vec<local_pbf> = entries
    .flatten()
    .filter(|e| {
      e.path()
        .extension()
        .map(|ext| ext == "pbf")
        .unwrap_or(false)
    })
    .filter_map(|e| {
      let size_bytes = e.metadata().ok()?.len();
      Some(local_pbf {
        path: e.path(),
        size_bytes,
      })
    })
    .collect();
  result.sort_by(|a, b| a.path.cmp(&b.path));
  result
}

#[cfg(test)]
#[path = "ls.test.rs"]
mod ls_test;