use std::collections::{BTreeMap, BTreeSet};

use url::Url;

use crate::manifest::{BuildMode, LinkMatchMode, Manifest};

#[derive(Debug, Clone)]
pub struct LinkPolicy {
    pub match_mode: LinkMatchMode,
    pub targets: BTreeSet<String>,
}

pub fn build_link_policies(
    manifest: &Manifest,
    entry_metadata: &BTreeMap<String, EntryLinkMetadata>,
) -> BTreeMap<String, LinkPolicy> {
    entry_metadata
        .iter()
        .map(|(entry_id, _metadata)| {
            let entry = &manifest.entries[entry_id];
            let mode = entry
                .links
                .mode
                .clone()
                .unwrap_or(manifest.link_rules.mode.clone());
            let targets = resolve_targets(manifest, entry_id, &mode);
            let match_mode = select_match_mode(manifest, entry_id, &mode);
            (
                entry_id.clone(),
                LinkPolicy {
                    match_mode,
                    targets,
                },
            )
        })
        .collect()
}

#[derive(Debug, Clone)]
pub struct EntryLinkMetadata {
    pub source_url: Option<Url>,
    pub canonical_url: Option<Url>,
}

fn resolve_targets(manifest: &Manifest, entry_id: &str, mode: &BuildMode) -> BTreeSet<String> {
    let entry = &manifest.entries[entry_id];
    let mut targets = BTreeSet::new();

    match mode {
        BuildMode::None => return targets,
        BuildMode::Auto => {
            for candidate in manifest.entries.keys() {
                if candidate != entry_id {
                    targets.insert(candidate.clone());
                }
            }
        }
        BuildMode::Explicit => {
            for rule in &manifest.link_rules.rules {
                if rule.match_mode == LinkMatchMode::Disabled {
                    continue;
                }
                if selector_matches_any(&rule.from, manifest, entry_id) {
                    for target in expand_selectors(&rule.to, manifest) {
                        if target != entry_id {
                            targets.insert(target);
                        }
                    }
                }
            }
        }
    }

    if !entry.links.allow_to.is_empty() {
        targets.retain(|candidate| entry.links.allow_to.contains(candidate));
    }

    for blocked in &entry.links.block_to {
        targets.remove(blocked);
    }

    targets
}

fn select_match_mode(manifest: &Manifest, entry_id: &str, mode: &BuildMode) -> LinkMatchMode {
    match mode {
        BuildMode::None => LinkMatchMode::Disabled,
        BuildMode::Auto => LinkMatchMode::CanonicalUrl,
        BuildMode::Explicit => manifest
            .link_rules
            .rules
            .iter()
            .find(|rule| selector_matches_any(&rule.from, manifest, entry_id))
            .map(|rule| rule.match_mode.clone())
            .unwrap_or(LinkMatchMode::CanonicalUrl),
    }
}

fn selector_matches_any(selectors: &[String], manifest: &Manifest, entry_id: &str) -> bool {
    selectors
        .iter()
        .any(|selector| selector_matches(selector, manifest, entry_id))
}

fn selector_matches(selector: &str, manifest: &Manifest, entry_id: &str) -> bool {
    if selector == "*" {
        return true;
    }
    if selector == entry_id {
        return true;
    }
    if let Some(section_id) = selector.strip_prefix("section:") {
        return manifest
            .sections
            .iter()
            .find(|section| section.id == section_id)
            .is_some_and(|section| section.entries.iter().any(|entry| entry == entry_id));
    }
    false
}

fn expand_selectors(selectors: &[String], manifest: &Manifest) -> BTreeSet<String> {
    let mut expanded = BTreeSet::new();
    for selector in selectors {
        if selector == "*" {
            expanded.extend(manifest.entries.keys().cloned());
            continue;
        }
        if let Some(section_id) = selector.strip_prefix("section:") {
            if let Some(section) = manifest
                .sections
                .iter()
                .find(|section| section.id == section_id)
            {
                expanded.extend(section.entries.iter().cloned());
            }
            continue;
        }
        if manifest.entries.contains_key(selector) {
            expanded.insert(selector.clone());
        }
    }
    expanded
}

pub fn matches_target(
    href: &Url,
    policy: &LinkPolicy,
    target_id: &str,
    metadata: &EntryLinkMetadata,
) -> bool {
    if !policy.targets.contains(target_id) {
        return false;
    }

    match policy.match_mode {
        LinkMatchMode::Disabled => false,
        LinkMatchMode::CanonicalUrl => metadata
            .canonical_url
            .as_ref()
            .is_some_and(|candidate| candidate.as_str() == href.as_str()),
        LinkMatchMode::SourceUrl => metadata
            .source_url
            .as_ref()
            .is_some_and(|candidate| candidate.as_str() == href.as_str()),
    }
}
