use std::fs;
use std::path::PathBuf;
use std::sync::Arc;

use anyhow::Context;
use clap::Parser;
use tracing::{error, info_span};
use tracing_subscriber::layer::SubscriberExt as _;
use tracing_subscriber::util::SubscriberInitExt as _;
use treehouse::cli::serve::serve;
use treehouse::config::VfsConfig;
use treehouse::dirs::Dirs;
use treehouse::generate;
use treehouse::sources::Sources;
use treehouse::vfs::asynch::AsyncDir;
use treehouse::vfs::{
    AnchoredAtExt, Blake3ContentVersionCache, Content, DynDir, ImageSizeCache, ToDynDir, VPathBuf,
};
use treehouse::vfs::{Cd, PhysicalDir};
use treehouse::{
    cli::{
        fix::{fix_all_cli, fix_file_cli},
        wc::wc_cli,
        Command, ProgramArgs,
    },
    vfs::{BufferedFile, MemDir, VPath},
};

fn vfs_sources(config: &VfsConfig) -> anyhow::Result<DynDir> {
    let mut root = MemDir::new();

    root.add(
        VPath::new("treehouse.toml"),
        BufferedFile::new(Content::new(
            "application/toml",
            fs::read("treehouse.toml")?,
        ))
        .to_dyn(),
    );
    root.add(
        VPath::new("static"),
        PhysicalDir::new(PathBuf::from("static"))
            .anchored_at(VPathBuf::new("static"))
            .to_dyn(),
    );
    root.add(
        VPath::new("template"),
        PhysicalDir::new(PathBuf::from("template")).to_dyn(),
    );
    root.add(
        VPath::new("content"),
        PhysicalDir::new(PathBuf::from("content")).to_dyn(),
    );

    let root = Blake3ContentVersionCache::new(config.cache_salt.as_bytes().to_owned(), root);
    let root = ImageSizeCache::new(root);

    Ok(root.to_dyn())
}

async fn fallible_main(
    args: ProgramArgs,
    flush_guard: Option<tracing_chrome::FlushGuard>,
) -> anyhow::Result<()> {
    let vfs_config = toml_edit::de::from_str(
        &fs::read_to_string("vfs.toml").context("failed to read vfs.toml")?,
    )
    .context("failed to deserialize vfs.toml")?;

    let src = vfs_sources(&vfs_config)?;
    let dirs = Arc::new(Dirs {
        root: src.clone(),
        content: Cd::new(src.clone(), VPathBuf::new("content")).to_dyn(),
        static_: Cd::new(src.clone(), VPathBuf::new("static")).to_dyn(),
        template: Cd::new(src.clone(), VPathBuf::new("template")).to_dyn(),
        pic: Cd::new(src.clone(), VPathBuf::new("static/pic")).to_dyn(),
        emoji: Cd::new(src.clone(), VPathBuf::new("static/emoji")).to_dyn(),
        syntax: Cd::new(src.clone(), VPathBuf::new("static/syntax")).to_dyn(),
    });

    match args.command {
        Command::Serve {
            generate: _,
            serve: serve_args,
        } => {
            let _span = info_span!("load").entered();
            let sources = Arc::new(Sources::load(&dirs).context("failed to load sources")?);
            let target = generate::target(dirs.clone(), sources.clone());
            drop(_span);
            drop(flush_guard);

            serve(sources, dirs, AsyncDir::new(target), serve_args.port).await?;
        }

        Command::Fix(fix_args) => fix_file_cli(fix_args, &*dirs.root)?.apply().await?,
        Command::FixAll(fix_args) => fix_all_cli(fix_args, &*dirs.content)?.apply().await?,

        Command::Wc(wc_args) => wc_cli(&dirs.content, wc_args)?,

        Command::Ulid => {
            let mut rng = rand::thread_rng();
            let ulid = ulid::Generator::new()
                .generate_with_source(&mut rng)
                .expect("failed to generate ulid");
            println!("{ulid}");
        }
    }

    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = ProgramArgs::parse();

    let (chrome_layer, flush_guard) = args
        .trace
        .as_ref()
        .map(|path| {
            tracing_chrome::ChromeLayerBuilder::new()
                .file(path)
                .include_args(true)
                .build()
        })
        .unzip();

    tracing_subscriber::registry()
        .with(tracing_subscriber::EnvFilter::new("treehouse=trace"))
        .with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
        .with(chrome_layer)
        .init();

    match fallible_main(args, flush_guard).await {
        Ok(_) => (),
        Err(error) => error!("fatal: {error:?}"),
    }

    Ok(())
}
