use std::{
    collections::{HashMap, HashSet},
    ops::ControlFlow,
};

use anyhow::{Context, anyhow};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use serde::{Deserialize, Serialize};
use tracing::{error, info_span, instrument};

use crate::{
    html::highlight::{
        Syntax,
        compiled::{CompiledSyntax, compile_syntax},
    },
    import_map::ImportRoot,
    vfs::{self, Content, Dir, DynDir, ImageSize, VPath, VPathBuf},
};

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct VfsConfig {
    /// Cache salt string. Passed to `Blake3ContentVersionCache` as a salt for content version hashes.
    /// Can be changed to bust cached assets for all clients.
    pub cache_salt: String,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Config {
    /// Website root; used when generating links.
    /// Can also be specified using the environment variable `$TREEHOUSE_SITE`. (this is the
    /// preferred way of setting this in production, so as not to clobber treehouse.toml.)
    pub site: String,

    /// This is used to generate a link in the footer that links to the page's source commit.
    /// The final URL is `{commit_base_url}/{commit}/content/{tree_path}.tree`.
    pub commit_base_url: String,

    /// User-defined keys.
    pub user: HashMap<String, String>,

    /// Links exported to Markdown for use with reference syntax `[text][def:key]`.
    pub defs: HashMap<String, String>,

    /// Config for syndication feeds.
    pub feed: Feed,

    /// Redirects for moving pages around. These are used solely by the treehouse server.
    ///
    /// Note that redirects are only resolved _non-recursively_ by the server. For a configuration
    /// like:
    ///
    /// ```toml
    /// page.redirects.foo = "bar"
    /// page.redirects.bar = "baz"
    /// ```
    ///
    /// the user will be redirected from `foo` to `bar`, then from `bar` to `baz`. This isn't
    /// optimal for UX and causes unnecessary latency. Therefore you should always make redirects
    /// point to the newest version of the page.
    ///
    /// ```toml
    /// page.redirects.foo = "baz"
    /// page.redirects.bar = "baz"
    /// ```
    pub redirects: Redirects,

    /// How the treehouse should be built.
    pub build: Build,

    /// Overrides for emoji names. Useful for setting up aliases.
    ///
    /// Paths are anchored within `static/emoji` and must not contain parent directories.
    #[serde(default)]
    pub emoji: HashMap<String, VPathBuf>,

    /// Overrides for pic filenames. Useful for setting up aliases.
    ///
    /// On top of this, pics are autodiscovered by walking the `static/pic` directory.
    /// Only the part before the first dash is treated as the pic's id.
    pub pics: HashMap<String, VPathBuf>,

    /// Syntax definitions.
    ///
    /// These are not part of the config file, but are loaded as part of site configuration from
    /// `static/syntax`.
    #[serde(skip)]
    pub syntaxes: HashMap<String, CompiledSyntax>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Feed {
    /// Allowed tags in feed entries.
    pub tags: HashSet<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Redirects {
    /// Path redirects. When a user requests a path, if they request `p`, they will be redirected
    /// to `path[p]` with a `301 Moved Permanently` status code.
    pub path: HashMap<VPathBuf, VPathBuf>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Build {
    /// Configuration for how JavaScript is compiled.
    pub javascript: JavaScript,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct JavaScript {
    /// Import roots to generate in the project's import map.
    pub import_roots: Vec<ImportRoot>,
}

impl Config {
    #[instrument(name = "Config::autopopulate_emoji", skip(self))]
    pub fn autopopulate_emoji(&mut self, dir: &dyn Dir) -> anyhow::Result<()> {
        vfs::walk_dir_rec(dir, VPath::ROOT, &mut |path| {
            if path.extension().is_some_and(is_image_file)
                && let Some(emoji_name) = path.file_stem()
                && !self.emoji.contains_key(emoji_name)
            {
                self.emoji.insert(emoji_name.to_owned(), path.to_owned());
            }

            ControlFlow::Continue(())
        });

        Ok(())
    }

    #[instrument(name = "Config::autopopulate_pics", skip(self))]
    pub fn autopopulate_pics(&mut self, dir: &dyn Dir) -> anyhow::Result<()> {
        vfs::walk_dir_rec(dir, VPath::ROOT, &mut |path| {
            if path.extension().is_some_and(is_image_file)
                && let Some(pic_name) = path.file_stem()
            {
                let pic_id = pic_name
                    .split_once('-')
                    .map(|(before_dash, _after_dash)| before_dash)
                    .unwrap_or(pic_name);

                if !self.pics.contains_key(pic_id) {
                    self.pics.insert(pic_id.to_owned(), path.to_owned());
                }
            }

            ControlFlow::Continue(())
        });
        Ok(())
    }

    pub fn page_url(&self, page: &str) -> String {
        let (page, hash) = page.split_once('#').unwrap_or((page, ""));

        // We don't want .dj appearing in URLs, though it exists as a disambiguator in [page:] links.
        let page = page.strip_suffix(".dj").unwrap_or(page);
        if !hash.is_empty() {
            format!("{}/{page}#{hash}", self.site)
        } else {
            format!("{}/{page}", self.site)
        }
    }

    pub fn pic_url(&self, pics_dir: &dyn Dir, id: &str) -> String {
        vfs::url(
            &self.site,
            pics_dir,
            self.pics
                .get(id)
                .map(|x| &**x)
                .unwrap_or(VPath::new("404.png")),
        )
        .expect("pics_dir is not anchored anywhere")
    }

    pub fn pic_size(&self, pics_dir: &dyn Dir, id: &str) -> Option<ImageSize> {
        self.pics
            .get(id)
            .and_then(|path| vfs::query::<ImageSize>(pics_dir, path))
    }

    /// Loads all syntax definition files.
    #[instrument(name = "Config::load_syntaxes", skip(self))]
    pub fn load_syntaxes(&mut self, dir: DynDir) -> anyhow::Result<()> {
        let mut paths = vec![];
        vfs::walk_dir_rec(&dir, VPath::ROOT, &mut |path| {
            if path.extension() == Some("json") {
                paths.push(path.to_owned());
            }
            ControlFlow::Continue(())
        });

        let syntaxes: Vec<_> = paths
            .par_iter()
            .flat_map(|path| {
                let name = path
                    .file_stem()
                    .expect("syntax file name should have a stem due to the .json extension");

                let result: Result<Syntax, _> = vfs::query::<Content>(&dir, path)
                    .ok_or_else(|| anyhow!("syntax .json is not a file"))
                    .and_then(|b| b.string().context("syntax .json contains invalid UTF-8"))
                    .and_then(|s| {
                        let _span = info_span!("Config::load_syntaxes::parse").entered();
                        serde_json::from_str(&s).context("could not deserialize syntax file")
                    });
                match result {
                    Ok(syntax) => {
                        let _span = info_span!("Config::load_syntaxes::compile", ?name).entered();
                        let compiled = compile_syntax(&syntax);
                        Some((name.to_owned(), compiled))
                    }
                    Err(err) => {
                        error!("error while loading syntax file `{path}`: {err}");
                        None
                    }
                }
            })
            .collect();

        for (name, compiled) in syntaxes {
            self.syntaxes.insert(name, compiled);
        }

        Ok(())
    }
}

pub fn is_image_file(extension: &str) -> bool {
    matches!(extension, "png" | "svg" | "jpg" | "jpeg" | "webp")
}
