// Copyright (C) 2026  The Software Heritage developers
// See the AUTHORS file at the top-level directory of this distribution
// License: GNU General Public License version 3, or any later version
// See top-level LICENSE file for more information

use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::Ordering;

use anyhow::{Result, bail};
use osv::schema::RangeType;
use serde::{Deserialize, Serialize};
use swh_contents::diff::diff::FileDiff;
use swh_graph::graph::{NodeId, SwhGraphWithProperties};
use swh_graph::labels::LabelNameId;
use swh_graph::properties;
use swh_graph_stdlib::collections::SmallNodeSet;

use super::SzzStats;
use crate::git;

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
/// A range of lines
pub struct LineRange(pub std::ops::Range<u32>);

#[derive(Clone, Debug)]
pub struct VulnerableFileLines {
    pub path: Box<[LabelNameId]>,
    pub lines: Vec<LineRange>,
}

/// Parses a range into `(introduced_at_zero, introduction_revs, fix_revs)`
pub fn parse_range_revs<G: SwhGraphWithProperties<Maps: properties::Maps>>(
    graph: &Arc<G>,
    range: &osv::schema::Range,
    stats: &SzzStats,
) -> Result<Option<(bool, SmallNodeSet, SmallNodeSet)>> {
    let git_range_parser = git::GitRangeParser::new(Arc::clone(graph));

    let RangeType::Git = range.range_type else {
        bail!("Unexpected range type: {range:?}")
    };
    let Some((mut revs, introduced_at_zero)) = git_range_parser.parse_git_range(range, None)?
    else {
        stats
            .ignored_because_unknown_commit
            .fetch_add(1, Ordering::Relaxed);
        return Ok(None);
    };
    if !revs[crate::EventType::LastAffected as usize].is_empty() {
        stats
            .ignored_because_last_affected
            .fetch_add(1, Ordering::Relaxed);
        return Ok(None);
    }
    if !revs[crate::EventType::Limit as usize].is_empty() {
        stats.ignored_because_limit.fetch_add(1, Ordering::Relaxed);
        return Ok(None);
    }
    let mut fix_revs = SmallNodeSet::default();
    std::mem::swap(&mut fix_revs, &mut revs[crate::EventType::Fixed as usize]);

    let mut introduction_revs = SmallNodeSet::default();
    std::mem::swap(
        &mut introduction_revs,
        &mut revs[crate::EventType::Introduced as usize],
    );

    Ok(Some((introduced_at_zero, introduction_revs, fix_revs)))
}

pub trait StrategyFactory {
    type Strategy<'s>: Strategy
    where
        Self: 's;

    fn get_instance<'s>(
        &'s self,
        vulnerability_filename: &'s str,
        stats: &'s SzzStats,
    ) -> Result<Self::Strategy<'s>>;
}

pub trait Strategy {
    /// Given a [range of revisions affected by a vulnerability](osv::schema::Range),
    /// returns which revision fixed the vulnerability
    fn get_fix_revs(&mut self, range: &osv::schema::Range) -> Result<Option<SmallNodeSet>>;

    /// Given a [range of revisions affected by a vulnerability](osv::schema::Range),
    /// returns whether the vulnerability was introduced since the beginning (`"introduced": "0"`
    /// in OSV documents) and which revisions introduced the vulnerability
    fn get_introduction_revs(
        &mut self,
        range: &osv::schema::Range,
    ) -> Result<Option<(bool, SmallNodeSet)>>;

    /// Given a list of changes made by a fix revision, returns which hunks (grouped by file)
    /// in the previous revision are vulnerable and should be traced back to an introducing revision
    ///
    /// Defaults to returning all hunks but those in files ignored according to
    /// [`Self::ignore_changed_content`]
    fn vulnerable_hunks(
        &mut self,
        fix_rev: NodeId,
        changes: &[&FileDiff],
    ) -> Result<impl Iterator<Item = Result<VulnerableFileLines>>> {
        let mut ignore_changed_paths = HashMap::new(); // cache

        Ok(changes
            .iter()
            .map(move |change| {
                Ok(match change {
                    FileDiff::Added { .. } => None,   // can't do anything with it
                    FileDiff::Removed { .. } => None, // TODO (how often does this happen in practice, though?)
                    FileDiff::Modified {
                        new_file,
                        old_file,
                        hunks: Some(hunks),
                    } => {
                        // For each changed file...
                        if hunks.is_empty() {
                            log::debug!(
                                "No meaningful difference between {} and {}",
                                old_file.swhid,
                                new_file.swhid
                            );
                        }

                        let ignore = match ignore_changed_paths.entry(old_file.path.clone()) {
                            std::collections::hash_map::Entry::Occupied(entry) => *entry.get(),
                            std::collections::hash_map::Entry::Vacant(entry) => {
                                let ignore = self.ignore_changed_content(
                                    fix_rev,
                                    &old_file.path,
                                    old_file.node,
                                    new_file.node,
                                )?;
                                entry.insert(ignore);
                                ignore
                            }
                        };

                        if ignore {
                            None
                        } else {
                            Some(VulnerableFileLines {
                                path: old_file.path.clone(),
                                lines: hunks
                                    .into_iter()
                                    .map(|hunk| LineRange(hunk.old_range.clone()))
                                    .collect(),
                            })
                        }
                    }
                    FileDiff::Modified {
                        new_file: _,
                        old_file: _,
                        hunks: None,
                    } => None, // TODO: binary file
                })
            })
            .flat_map(|res: Result<Option<_>>| -> Option<Result<_>> { res.transpose() }))
    }

    /// Returns whether a file at the given `path` changed by `rev` is not meaningful to
    /// vulnerability analysis
    ///
    /// Defaults to not ignoring anything
    fn ignore_changed_content(
        &mut self,
        _rev: NodeId,
        _path: &[LabelNameId],
        _old_cnt: NodeId,
        _new_cnt: NodeId,
    ) -> Result<bool> {
        Ok(false)
    }
}

pub struct NaiveStrategyFactory<G> {
    graph: Arc<G>,
}

impl<G> NaiveStrategyFactory<G> {
    pub fn new(graph: Arc<G>) -> Self {
        Self { graph }
    }
}

impl<G: SwhGraphWithProperties<Maps: properties::Maps>> StrategyFactory
    for NaiveStrategyFactory<G>
{
    type Strategy<'s>
        = NaiveStrategy<'s, G>
    where
        Self: 's;

    fn get_instance<'s>(
        &'s self,
        _vulnerability_filename: &'s str,
        stats: &'s SzzStats,
    ) -> Result<Self::Strategy<'s>> {
        Ok(NaiveStrategy {
            graph: self.graph.clone(),
            stats,
        })
    }
}

pub struct NaiveStrategy<'s, G> {
    graph: Arc<G>,
    stats: &'s SzzStats,
}

impl<G: SwhGraphWithProperties<Maps: properties::Maps>> Strategy for NaiveStrategy<'_, G> {
    fn get_fix_revs(&mut self, range: &osv::schema::Range) -> Result<Option<SmallNodeSet>> {
        parse_range_revs(&self.graph, range, self.stats)
            .map(|option| option.map(|(_intro_at_zero, _intro_revs, fix_rev)| fix_rev))
    }

    fn get_introduction_revs(
        &mut self,
        range: &osv::schema::Range,
    ) -> Result<Option<(bool, SmallNodeSet)>> {
        parse_range_revs(&self.graph, range, self.stats).map(|option| {
            option.map(|(intro_at_zero, intro_revs, _fix_rev)| (intro_at_zero, intro_revs))
        })
    }
}
