/*
 * Copyright(c) 2024-2026 China Telecom Cloud Technologies Co., Ltd. All rights
 * reserved. ctscat is licensed under Mulan PSL v2. You can use this software
 * according to the terms and conditions of the Mulan PSL V2. You may obtain a
 * copy of Mulan PSL v2 at: http://license.coscl.org.cn/MulanPSL2.
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY
 * KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.  See the Mulan PSL v2 for
 * more details.
 */

use anyhow::Result;
use tracing::info;

use crate::snapshot::types::{FileEntry, RepositorySnapshot, SpecEntry};

pub mod comparison_service;
pub mod git_client;
pub mod l1_vs_l0;
pub mod l2_vs_l1;
pub mod types;

pub use comparison_service::ComparisonService;
pub use git_client::GitRepositoryClient;
pub use l1_vs_l0::{
    CveAnalysis, CveInfo, L0VersionInfo, L1VersionInfo, L1VsL0Comparator, L1VsL0Report,
    PatchAnalysis, PatchInfo, UpgradableVersion, VersionTag,
};
pub use l2_vs_l1::{
    ConflictType, Customization, CustomizationAnalysis, CustomizationType, EffortLevel, L1Snapshot,
    L2Snapshot, L2VsL1Comparator, L2VsL1Report, MergeConflict, PatchDiff, PatchFile,
    PatchModification, SourceDiff, SourceFile, SourceModification, SpecDiff as L2VsL1SpecDiff,
    SyncPriority, SyncRecommendation, SyncType, VersionDiff, VersionRelationship,
};
use types::{DiffReport, FileDiff, SpecDiff, SummaryDiff};

pub fn diff_snapshots(l1: &RepositorySnapshot, l2: &RepositorySnapshot) -> Result<DiffReport> {
    info!(tracking_id = l1.tracking_id, "computing diff snapshots");

    let file_diff = diff_files(&l1.files, &l2.files);
    let spec_diff = diff_spec(l1.spec.as_ref(), l2.spec.as_ref());
    let summary = SummaryDiff {
        l1_commits: l1.commits.len(),
        l2_commits: l2.commits.len(),
        l1_issues: l1.issues.len(),
        l2_issues: l2.issues.len(),
    };

    Ok(DiffReport {
        tracking_id: l1.tracking_id,
        generated_at: l1.generated_at,
        file_diff,
        spec_diff,
        summary,
    })
}

fn diff_files(l1_files: &[FileEntry], l2_files: &[FileEntry]) -> Vec<FileDiff> {
    use std::collections::HashMap;

    let mut map_l2 = HashMap::new();
    for file in l2_files {
        map_l2.insert(&file.path, file);
    }

    let mut diffs = Vec::new();
    for file in l1_files {
        if let Some(other) = map_l2.remove(&file.path) {
            if file.sha256 != other.sha256 {
                diffs.push(FileDiff::Modified {
                    path: file.path.clone(),
                    l1_sha: file.sha256.clone(),
                    l2_sha: other.sha256.clone(),
                });
            }
        } else {
            diffs.push(FileDiff::Added {
                path: file.path.clone(),
                sha: file.sha256.clone(),
            });
        }
    }

    for remaining in map_l2.values() {
        diffs.push(FileDiff::Deleted {
            path: remaining.path.clone(),
            sha: remaining.sha256.clone(),
        });
    }

    diffs
}

fn diff_spec(l1_spec: Option<&SpecEntry>, l2_spec: Option<&SpecEntry>) -> Option<SpecDiff> {
    match (l1_spec, l2_spec) {
        (Some(lhs), Some(rhs)) => match (&lhs.version, &rhs.version) {
            (Some(v1), Some(v2)) if v1 == v2 => None,
            (Some(v1), Some(v2)) => Some(SpecDiff::Modified {
                l1_version: Some(v1.clone()),
                l2_version: Some(v2.clone()),
                l1_sha: lhs.sha256.clone(),
                l2_sha: rhs.sha256.clone(),
            }),
            _ if lhs.sha256 == rhs.sha256 => None,
            _ => Some(SpecDiff::Modified {
                l1_version: lhs.version.clone(),
                l2_version: rhs.version.clone(),
                l1_sha: lhs.sha256.clone(),
                l2_sha: rhs.sha256.clone(),
            }),
        },
        (Some(lhs), None) => Some(SpecDiff::Added {
            version: lhs.version.clone(),
            sha: lhs.sha256.clone(),
        }),
        (None, Some(rhs)) => Some(SpecDiff::Deleted {
            version: rhs.version.clone(),
            sha: rhs.sha256.clone(),
        }),
        (None, None) => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::snapshot::types::{FileEntry, RepositorySnapshot, SnapshotOrigin, SpecEntry};

    #[test]
    fn test_diff_files_modified() {
        let l1_files = vec![
            FileEntry {
                path: "file1.txt".to_string(),
                sha256: "sha1".to_string(),
                size: 100,
                is_binary: false,
            },
            FileEntry {
                path: "file2.txt".to_string(),
                sha256: "sha2".to_string(),
                size: 200,
                is_binary: false,
            },
        ];
        let l2_files = vec![
            FileEntry {
                path: "file1.txt".to_string(),
                sha256: "sha1_modified".to_string(),
                size: 100,
                is_binary: false,
            },
            FileEntry {
                path: "file2.txt".to_string(),
                sha256: "sha2".to_string(),
                size: 200,
                is_binary: false,
            },
        ];

        let diff = diff_files(&l1_files, &l2_files);
        assert_eq!(diff.len(), 1);
        if let FileDiff::Modified {
            path,
            l1_sha,
            l2_sha,
        } = &diff[0]
        {
            assert_eq!(path, "file1.txt");
            assert_eq!(l1_sha, "sha1");
            assert_eq!(l2_sha, "sha1_modified");
        } else {
            panic!("Expected Modified diff");
        }
    }

    #[test]
    fn test_diff_files_added_and_deleted() {
        let l1_files = vec![FileEntry {
            path: "file1.txt".to_string(),
            sha256: "sha1".to_string(),
            size: 100,
            is_binary: false,
        }];
        let l2_files = vec![FileEntry {
            path: "file2.txt".to_string(),
            sha256: "sha2".to_string(),
            size: 200,
            is_binary: false,
        }];

        let diff = diff_files(&l1_files, &l2_files);
        assert_eq!(diff.len(), 2);

        let added = diff.iter().find(|d| matches!(d, FileDiff::Added { .. }));
        let deleted = diff.iter().find(|d| matches!(d, FileDiff::Deleted { .. }));

        assert!(added.is_some());
        assert!(deleted.is_some());

        if let Some(FileDiff::Added { path, sha }) = added {
            assert_eq!(path, "file1.txt");
            assert_eq!(sha, "sha1");
        }

        if let Some(FileDiff::Deleted { path, sha }) = deleted {
            assert_eq!(path, "file2.txt");
            assert_eq!(sha, "sha2");
        }
    }

    #[test]
    fn test_diff_spec_same_version() {
        let l1_spec = SpecEntry {
            path: "test.spec".to_string(),
            sha256: "sha1".to_string(),
            version: Some("1.0.0".to_string()),
            release: Some("1".to_string()),
            content_base64: "".to_string(),
        };
        let l2_spec = SpecEntry {
            path: "test.spec".to_string(),
            sha256: "sha2".to_string(),         // sha changed
            version: Some("1.0.0".to_string()), // version same
            release: Some("1".to_string()),
            content_base64: "".to_string(),
        };

        let diff = diff_spec(Some(&l1_spec), Some(&l2_spec));
        assert!(diff.is_none());
    }

    #[test]
    fn test_diff_spec_version_changed() {
        let l1_spec = SpecEntry {
            path: "test.spec".to_string(),
            sha256: "sha1".to_string(),
            version: Some("1.0.1".to_string()),
            release: Some("1".to_string()),
            content_base64: "".to_string(),
        };
        let l2_spec = SpecEntry {
            path: "test.spec".to_string(),
            sha256: "sha2".to_string(),
            version: Some("1.0.0".to_string()),
            release: Some("1".to_string()),
            content_base64: "".to_string(),
        };

        let diff = diff_spec(Some(&l1_spec), Some(&l2_spec));
        assert!(diff.is_some());
        if let Some(SpecDiff::Modified {
            l1_version,
            l2_version,
            ..
        }) = diff
        {
            assert_eq!(l1_version, Some("1.0.1".to_string()));
            assert_eq!(l2_version, Some("1.0.0".to_string()));
        } else {
            panic!("Expected Modified diff");
        }
    }

    #[test]
    fn test_diff_spec_added_deleted() {
        let spec = SpecEntry {
            path: "test.spec".to_string(),
            sha256: "sha1".to_string(),
            version: Some("1.0.0".to_string()),
            release: Some("1".to_string()),
            content_base64: "".to_string(),
        };

        // Added (L1 has, L2 none)
        let diff_added = diff_spec(Some(&spec), None);
        assert!(matches!(diff_added, Some(SpecDiff::Added { .. })));

        // Deleted (L1 none, L2 has)
        let diff_deleted = diff_spec(None, Some(&spec));
        assert!(matches!(diff_deleted, Some(SpecDiff::Deleted { .. })));
    }

    #[test]
    fn test_diff_snapshots_integration() {
        let l1 = RepositorySnapshot::new(1, SnapshotOrigin::L1);
        let l2 = RepositorySnapshot::new(1, SnapshotOrigin::L2);

        let result = diff_snapshots(&l1, &l2);
        assert!(result.is_ok());
        let report = result.unwrap();
        assert_eq!(report.tracking_id, 1);
        assert_eq!(report.summary.l1_commits, 0);
        assert_eq!(report.summary.l2_commits, 0);
    }
}