use crate::{OrdinaryConfig, TemplateConfig, TemplateRef, TemplateRefField};
use anyhow::bail;
use hashbrown::{HashMap, HashSet};
use ordinary_types::{Field, Kind, json_to_flexbuffer_vec};
use regex::Regex;
use tracing::Span;
use url::Url;

struct NestedFields(HashMap<String, Option<NestedFields>>);

pub static DOMAIN_REGEX: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
    Regex::new(r"^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$")
        .expect("failed to create regex")
});

#[allow(clippy::too_many_lines)]
pub fn validate(config: &OrdinaryConfig) -> anyhow::Result<()> {
    let port = config.port.unwrap_or(433);
    let redirect_port = config.redirect_port.unwrap_or(80);

    if port == redirect_port {
        bail!("port and redirect_port cannot be same value {port}");
    }

    if !DOMAIN_REGEX.is_match(&config.domain) {
        bail!("'domain' field is not a valid domain name");
    }

    if let Some(cnames) = &config.cnames {
        if cnames.contains(&config.domain) {
            bail!("CNAMEs cannot include the primary domain");
        }

        for cname in cnames {
            if !DOMAIN_REGEX.is_match(cname) {
                bail!("cname '{cname}' is not valid domain name");
            }
        }
    }

    if let Some(redirects) = &config.redirects {
        if let Some(host_redirects) = &redirects.host {
            let mut from_set = HashSet::new();
            let mut to_set = HashSet::new();

            let mut host_set = HashSet::new();
            host_set.insert(config.domain.clone());

            if let Some(cnames) = &config.cnames {
                for cname in cnames {
                    host_set.insert(cname.clone());
                }
            }

            for host_redirect in host_redirects {
                if !DOMAIN_REGEX.is_match(&host_redirect.from) {
                    bail!(
                        "host redirect from '{}' is not valid domain name",
                        host_redirect.from
                    );
                }

                if !DOMAIN_REGEX.is_match(&host_redirect.to) {
                    bail!(
                        "host redirect to '{}' is not valid domain name",
                        host_redirect.to
                    );
                }

                if !host_set.contains(&host_redirect.from) {
                    bail!(
                        "'{}' for from is not in 'cnames' or the 'domain' field",
                        host_redirect.from
                    );
                }

                if !host_set.contains(&host_redirect.to) {
                    bail!(
                        "'{}' for to is not in 'cnames' or the 'domain' field",
                        host_redirect.from
                    );
                }

                if from_set.contains(&host_redirect.from) {
                    bail!(
                        "cannot have multiple 'from' host redirect rules for a single host {}",
                        host_redirect.from
                    );
                }
                from_set.insert(host_redirect.from.clone());

                if to_set.contains(&host_redirect.from) {
                    bail!(
                        "cannot have chained to/from redirects '{}'",
                        host_redirect.from
                    );
                }

                if from_set.contains(&host_redirect.to) {
                    bail!(
                        "cannot have chained to/from redirects '{}'",
                        host_redirect.to
                    );
                }

                to_set.insert(host_redirect.to.clone());
            }
        }

        if let Some(route_redirects) = &redirects.route {
            for route_redirect in route_redirects {
                if !route_redirect.condition.starts_with('/') {
                    bail!(
                        "route redirect condition {} must start with a leading forward slash",
                        route_redirect.condition
                    );
                }
            }
        }
    }

    if let Some(proxies) = &config.proxies {
        for proxy in proxies {
            if proxy.path.is_none() && proxy.domain.is_none() && proxy.port.is_none() {
                bail!(
                    "neither the proxy `path` nor the proxy `domain` nor the proxy `port` is set for target '{}'",
                    proxy.target
                );
            }

            if let Some(proxy_port) = &proxy.port {
                if proxy_port == &port {
                    bail!(
                        "proxy port {proxy_port} for target {} cannot be the same as primary port",
                        proxy.target
                    )
                }

                if proxy_port == &redirect_port {
                    bail!(
                        "proxy port {proxy_port} for target {} cannot be the same as redirect_port",
                        proxy.target
                    )
                }
            }

            if proxy.path.is_some() && proxy.domain.is_some() {
                tracing::warn!(
                    "`path` {} and `domain` {} should not be set at the same time, only one or the other should be picked; if both are set, only the `path` will be evaluated",
                    proxy.path.as_ref().unwrap_or(&String::new()),
                    proxy.domain.as_ref().unwrap_or(&String::new()),
                );
            }

            if let Some(custom_domain) = &proxy.domain {
                if !DOMAIN_REGEX.is_match(custom_domain) {
                    bail!("proxy domain '{custom_domain}' is not valid domain name");
                }

                if custom_domain == &config.domain {
                    bail!(
                        "proxy domain {custom_domain} for target {} is the same as the primary app domain.",
                        proxy.target
                    );
                }

                if let Some(cnames) = &config.cnames
                    && cnames.contains(custom_domain)
                {
                    bail!(
                        "proxy domain {custom_domain} for target {} contained in CNAMEs {cnames:?}",
                        proxy.target
                    );
                }

                let port_str = format!(":{port}");
                let redirect_port_str = format!(":{redirect_port}");

                if (custom_domain.starts_with("localhost")
                    || custom_domain.starts_with("127.0.0.1"))
                    && (custom_domain.contains(&port_str)
                        || custom_domain.contains(&redirect_port_str))
                {
                    bail!(
                        "loopback domain {custom_domain} port for target {} collide with primary {port_str} or redirect {redirect_port_str} ports",
                        proxy.target
                    );
                }
            }

            if let Some(path) = &proxy.path
                && !path.ends_with("/{*path}")
            {
                bail!(
                    "path for target '{}' must end in `/{{*path}}`",
                    proxy.target
                );
            }

            Url::parse(&proxy.target)?;
        }
    }

    if let Some(assets) = &config.assets {
        if !assets.base_route.starts_with('/') {
            bail!("assets base route must start with leading forward slash");
        }

        if assets.append_index_html == Some(true) && assets.append_html_ext == Some(true) {
            tracing::warn!(
                "'assets.append_index_html' and 'assets.append_index_ext' will conflict at runtime with 'assets.append_index_html' taking precedence. only one or the other should be selected."
            );
        }
    }

    if let Some(globals) = &config.globals {
        if globals.is_empty() {
            tracing::error!("empty 'globals' serves no purpose");
            bail!("empty 'globals' serves no purpose");
        }

        let mut set = HashSet::new();

        let mut builder = flexbuffers::Builder::new(&flexbuffers::BuilderOptions::SHARE_NONE);
        let mut builder_vec = builder.start_vector();

        for global in globals {
            match global.kind {
                Kind::List { .. } | Kind::Object { .. } => {
                    tracing::error!(name = %global.name, "cannot include nested structures in global");
                    bail!(
                        "cannot include nested structures in global '{}'",
                        global.name
                    );
                }
                _ => (),
            }

            json_to_flexbuffer_vec(&global.kind, &global.value, &mut builder_vec)?;

            if set.contains(&global.name) {
                tracing::error!(name = %global.name, "globals cannot have duplicated names");
                bail!("globals cannot have duplicated names '{}'", global.name);
            }
            set.insert(global.name.clone());
        }
    }

    if let Some(content) = &config.content {
        if content.definitions.is_empty() {
            tracing::error!("empty 'content.definitions' serves no purpose");
            bail!("empty 'content.definitions' serves no purpose");
        }

        let span = tracing::debug_span!(
            "content_def",
            name = tracing::field::Empty,
            idx = tracing::field::Empty
        );

        let name_idx_fields_pairs = content
            .definitions
            .iter()
            .map(|cd| (cd.name.clone(), cd.idx, Some(cd.fields.clone())))
            .collect::<Vec<(String, u8, Option<Vec<Field>>)>>();

        check_defs("content definition", &span, &name_idx_fields_pairs)?;
    }

    if let Some(templates) = &config.templates {
        if templates.is_empty() {
            tracing::error!("empty 'templates' serves no purpose");
            bail!("empty 'templates' serves no purpose");
        }

        let span = tracing::debug_span!(
            "template",
            name = tracing::field::Empty,
            idx = tracing::field::Empty
        );

        let name_idx_fields_pairs = templates
            .iter()
            .map(|cd| (cd.name.clone(), cd.idx, None))
            .collect::<Vec<(String, u8, Option<Vec<Field>>)>>();

        check_defs("template", &span, &name_idx_fields_pairs)?;

        for template in templates {
            let span = tracing::debug_span!("template", name = %template.name, idx = template.idx);

            span.in_scope(|| {
                if !template.route.starts_with('/') {
                    tracing::error!(route = %template.route, "route must start with a leading forward slash");
                    bail!("template: {} ({}) -- route '{}' must start with a leading forward slash", template.name, template.idx, template.route)
                }

                if let Some(template_globals) = &template.globals {
                    if template_globals.is_empty() {
                        tracing::error!("empty 'globals' serves no purpose");
                        bail!("template: {} ({}) -- empty 'globals' serves no purpose", template.name, template.idx);
                    }

                    if let Some(top_globals) = &config.globals {
                        let top_globals_set = top_globals
                            .iter()
                            .map(|g| g.name.clone())
                            .collect::<HashSet<String>>();

                        for template_global in template_globals {
                            if !top_globals_set.contains(template_global) {
                                tracing::error!(global = template_global, options = ?top_globals_set, "does not exist");
                                bail!("template: {} ({}) -- global '{template_global}' is not defined on the ordinary.json. valid options are: {top_globals_set:?}", template.name, template.idx);
                            }
                        }
                    } else {
                        tracing::error!("no globals defined in the ordinary.json");
                        bail!("template: {} ({}) -- no globals defined in the ordinary.json", template.name, template.idx);
                    }
                }

                if let Some(template_fields) = &template.fields {
                    if template_fields.is_empty() {
                        tracing::error!("empty 'fields' serves no purpose");
                        bail!("template: {} ({}) -- empty 'fields' serves no purpose", template.name, template.idx);
                    }

                    let mut set = HashSet::new();

                    let mut builder = flexbuffers::Builder::new(&flexbuffers::BuilderOptions::SHARE_NONE);
                    let mut builder_vec = builder.start_vector();

                    for field in template_fields {
                        match field.kind {
                            Kind::List { .. } | Kind::Object { .. } => {
                                tracing::error!(name = %field.name, "cannot include nested structures in template field");
                                bail!("template: {} ({}) -- cannot include nested structures in template field '{}'", template.name, template.idx, field.name);
                            },
                            _ => ()
                        }

                        json_to_flexbuffer_vec(&field.kind, &field.value, &mut builder_vec)?;

                        if set.contains(&field.name) {
                            tracing::error!(name = %field.name, "fields cannot have duplicated names");
                            bail!("template: {} ({}) -- fields cannot have duplicated names '{}'", template.name, template.idx, field.name);
                        }
                        set.insert(field.name.clone());
                    }
                }

                if let Some(template_content) = &template.content {
                    if template_content.is_empty() {
                        tracing::error!("empty 'content' serves no purpose");
                        bail!("template: {} ({}) -- empty 'content' serves no purpose", template.name, template.idx);
                    }

                    let name_idx_fields_pairs = template_content
                        .iter()
                        .map(|cd| (cd.name.clone(), cd.idx, cd.fields.clone()))
                        .collect::<Vec<(String, u8, Vec<TemplateRefField>)>>();

                    check_refs(&format!("'{}' template content", template.name), &span, &name_idx_fields_pairs)?;

                    if let Some(content) = &config.content {
                        let mut content_map: HashMap<String, NestedFields> = HashMap::new();

                        for content_def in &content.definitions {
                            let fields = content_def_field_builder(&content_def.fields);
                            content_map.insert(content_def.name.clone(), fields);
                        }

                        for template_ref in template_content {
                            if let Some(content_fields) = content_map.get(&template_ref.name) {
                                template_ref_field_checker(content_fields, &template_ref.fields, template_ref, template, &[])?;
                            } else {
                                tracing::error!(content_def = %template_ref.name, options = ?content_map.keys(), "does not exist");
                                bail!("template: {} ({}) -- content_def '{}' is not defined on the ordinary.json. valid options are: {:?}", template.name, template.idx, template_ref.name, content_map.keys());
                            }
                        }
                    } else {
                        tracing::error!("no content defined in the ordinary.json");
                        bail!("template: {} ({}) -- no content defined in the ordinary.json", template.name, template.idx);
                    }
                }

                Ok(())
            })?;
        }
    }

    Ok(())
}

fn check_defs(
    top_name: &str,
    span: &Span,
    name_idx_fields_pairs: &Vec<(String, u8, Option<Vec<Field>>)>,
) -> anyhow::Result<()> {
    let mut def_idx = vec![];
    let mut def_names = HashMap::new();

    for (name, idx, fields) in name_idx_fields_pairs {
        span.record("name", tracing::field::display(name));
        span.record("idx", idx);

        span.in_scope(|| {
            if let Some(collision_idx) = def_names.get(name) {
                tracing::error!(
                            collision = collision_idx,
                            "cannot duplicate names"
                        );
                bail!(
                            "cannot duplicate {top_name} names '{name}' for idx {collision_idx} and {idx}",
                        );
            }

            def_idx.push(idx);
            def_names.insert(name.clone(), idx);

            // todo: recurse nested
            if let Some(fields) = fields {
                if fields.is_empty() {
                    tracing::error!("empty 'fields' serves no purpose");
                    bail!("empty 'fields' serves no purpose on {top_name}");
                }

                let mut field_idx = vec![];
                let mut field_names = HashMap::new();

                for field in fields {
                    let span = tracing::debug_span!("field", name = %field.name, idx = field.idx);

                    span.in_scope(|| {
                        if let Some(collision_idx) = field_names.get(&field.name) {
                            tracing::error!(
                                collision = collision_idx,
                                name = field.name,
                                "cannot duplicate field names"
                            );
                            bail!(
                                "cannot duplicate '{name}' field names '{}' for idx {collision_idx} and {} on {top_name}",
                                field.name,
                                field.idx,
                            );
                        }

                        field_idx.push(field.idx);
                        field_names.insert(field.name.clone(), field.idx);

                        Ok(())
                    })?;
                }

                field_idx.sort_unstable();

                for (i, idx) in field_idx.iter().enumerate() {
                    if i != *idx as usize {
                        tracing::error!("field idx must start at 0 and cannot have gaps");
                        bail!("'{name}' field idx must start at 0 and cannot have gaps on {top_name}")
                    }
                }
            }

            Ok(())
        })?;

        def_idx.sort_unstable();

        for (i, idx) in def_idx.iter().enumerate() {
            if i != **idx as usize {
                tracing::error!("{top_name} idx must start at 0 and cannot have gaps");
                bail!("{top_name} idx must start at 0 and cannot have gaps");
            }
        }
    }

    Ok(())
}

fn check_refs(
    top_name: &str,
    span: &Span,
    name_idx_fields_pairs: &Vec<(String, u8, Vec<TemplateRefField>)>,
) -> anyhow::Result<()> {
    let mut def_idx = vec![];
    let mut def_names = HashMap::new();

    for (name, idx, fields) in name_idx_fields_pairs {
        span.record("name", tracing::field::display(name));
        span.record("idx", idx);

        span.in_scope(|| {
            if fields.is_empty() {
                tracing::error!(content_def = %name, "empty 'fields' serves no purpose");
                bail!("empty content '{name}' 'fields' serves no purpose on {top_name}");
            }

            if let Some(collision_idx) = def_names.get(name) {
                tracing::error!(
                            collision = collision_idx,
                            "cannot duplicate names"
                        );
                bail!(
                            "cannot duplicate {top_name} names '{name}' for idx {collision_idx} and {idx}",
                        );
            }

            def_idx.push(idx);
            def_names.insert(name.clone(), idx);

            let mut field_idx = vec![];
            let mut field_names = HashMap::new();

            // todo: recurse nested
            for field in fields {
                let span = tracing::debug_span!("field", name = %field.name, idx = field.idx);

                span.in_scope(|| {
                    if let Some(collision_idx) = field_names.get(&field.name) {
                        tracing::error!(
                                collision = collision_idx,
                                name = field.name,
                                "cannot duplicate field names"
                            );
                        bail!(
                                "cannot duplicate '{name}' field names '{}' for idx {collision_idx} and {} on {top_name}",
                                field.name,
                                field.idx,
                            );
                    }

                    field_idx.push(field.idx);
                    field_names.insert(field.name.clone(), field.idx);

                    Ok(())
                })?;
            }

            field_idx.sort_unstable();

            for (i, idx) in field_idx.iter().enumerate() {
                if i != *idx as usize {
                    tracing::error!("field idx must start at 0 and cannot have gaps");
                    bail!("'{name}' field idx must start at 0 and cannot have gaps on {top_name}");
                }
            }

            Ok(())
        })?;

        def_idx.sort_unstable();

        for (i, idx) in def_idx.iter().enumerate() {
            if i != **idx as usize {
                tracing::error!("{top_name} idx must start at 0 and cannot have gaps");
                bail!("{top_name} idx must start at 0 and cannot have gaps");
            }
        }
    }

    Ok(())
}

fn content_def_field_builder(fields: &Vec<Field>) -> NestedFields {
    let mut map = NestedFields(HashMap::new());

    for field in fields {
        match &field.kind {
            Kind::Object { fields, .. } => map
                .0
                .insert(field.name.clone(), Some(content_def_field_builder(fields))),
            Kind::List { kind } => match &**kind {
                // todo: handle lists of lists
                Kind::Object { fields, .. } => map
                    .0
                    .insert(field.name.clone(), Some(content_def_field_builder(fields))),
                _ => map.0.insert(field.name.clone(), None),
            },
            _ => map.0.insert(field.name.clone(), None),
        };
    }

    map
}

fn template_ref_field_checker(
    content_fields: &NestedFields,
    template_fields: &Vec<TemplateRefField>,
    template_ref: &TemplateRef,
    template: &TemplateConfig,
    field_chain: &[String],
) -> anyhow::Result<()> {
    if template_fields.is_empty() {
        tracing::error!(field = %field_chain.join("."), content_def = %template_ref.name, "empty 'fields' serves no purpose");
        bail!(
            "template: {} ({}) -- empty 'fields' serves no purpose on {}",
            template.name,
            template.idx,
            template_ref.name
        );
    }

    for template_field in template_fields {
        let mut field_chain = field_chain.to_vec();
        field_chain.push(template_field.name.clone());

        if let Some(nested_fields) = content_fields.0.get(&template_field.name) {
            if let Some(nested_fields) = nested_fields {
                if let Some(template_fields) = &template_field.fields {
                    template_ref_field_checker(
                        nested_fields,
                        template_fields,
                        template_ref,
                        template,
                        &field_chain,
                    )?;
                } else {
                    tracing::error!(field = %field_chain.join("."), content_def = %template_ref.name, options = ?nested_fields.0.keys(), "no nested fields provided");
                    bail!(
                        "template: {} ({}) -- field '{}' does not list any nested fields '{}'. include some of the following: {:?}",
                        template.name,
                        template.idx,
                        field_chain.join("."),
                        template_ref.name,
                        nested_fields.0.keys()
                    );
                }
            } else if !content_fields.0.contains_key(&template_field.name) {
                tracing::error!(field = %field_chain.join("."), content_def = %template_ref.name, options = ?content_fields.0.keys(), "does not exist");
                bail!(
                    "template: {} ({}) -- field '{}' is not defined on the content_def '{}'. valid options are: {:?}",
                    template.name,
                    template.idx,
                    field_chain.join("."),
                    template_ref.name,
                    content_fields.0.keys()
                );
            }
        } else {
            tracing::error!(field = %field_chain.join("."), content_def = %template_ref.name, options = ?content_fields.0.keys(), "does not exist");
            bail!(
                "template: {} ({}) -- field '{}' is not defined on the content_def '{}'. valid options are: {:?}",
                template.name,
                template.idx,
                field_chain.join("."),
                template_ref.name,
                content_fields.0.keys()
            );
        }
    }

    Ok(())
}
