use compliance_core::models::TrackerType;
use serde::{Deserialize, Serialize};

use compliance_core::models::ScanRun;

#[derive(Deserialize)]
pub struct PaginationParams {
    #[serde(default = "default_page")]
    pub page: u64,
    #[serde(default = "default_limit")]
    pub limit: i64,
}

pub(crate) fn default_page() -> u64 {
    1
}
pub(crate) fn default_limit() -> i64 {
    50
}

#[derive(Deserialize)]
pub struct FindingsFilter {
    #[serde(default)]
    pub repo_id: Option<String>,
    #[serde(default)]
    pub severity: Option<String>,
    #[serde(default)]
    pub scan_type: Option<String>,
    #[serde(default)]
    pub status: Option<String>,
    #[serde(default)]
    pub q: Option<String>,
    #[serde(default)]
    pub sort_by: Option<String>,
    #[serde(default)]
    pub sort_order: Option<String>,
    #[serde(default = "default_page")]
    pub page: u64,
    #[serde(default = "default_limit")]
    pub limit: i64,
}

#[derive(Serialize)]
pub struct ApiResponse<T: Serialize> {
    pub data: T,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub total: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub page: Option<u64>,
}

#[derive(Serialize)]
pub struct OverviewStats {
    pub total_repositories: u64,
    pub total_findings: u64,
    pub critical_findings: u64,
    pub high_findings: u64,
    pub medium_findings: u64,
    pub low_findings: u64,
    pub total_sbom_entries: u64,
    pub total_cve_alerts: u64,
    pub total_issues: u64,
    pub recent_scans: Vec<ScanRun>,
}

#[derive(Deserialize)]
pub struct AddRepositoryRequest {
    pub name: String,
    pub git_url: String,
    #[serde(default = "default_branch")]
    pub default_branch: String,
    pub auth_token: Option<String>,
    pub auth_username: Option<String>,
    pub tracker_type: Option<TrackerType>,
    pub tracker_owner: Option<String>,
    pub tracker_repo: Option<String>,
    pub tracker_token: Option<String>,
    pub scan_schedule: Option<String>,
}

#[derive(Deserialize)]
pub struct UpdateRepositoryRequest {
    pub name: Option<String>,
    pub default_branch: Option<String>,
    pub auth_token: Option<String>,
    pub auth_username: Option<String>,
    pub tracker_type: Option<TrackerType>,
    pub tracker_owner: Option<String>,
    pub tracker_repo: Option<String>,
    pub tracker_token: Option<String>,
    pub scan_schedule: Option<String>,
}

fn default_branch() -> String {
    "main".to_string()
}

#[derive(Deserialize)]
pub struct UpdateStatusRequest {
    pub status: String,
}

#[derive(Deserialize)]
pub struct BulkUpdateStatusRequest {
    pub ids: Vec<String>,
    pub status: String,
}

#[derive(Deserialize)]
pub struct UpdateFeedbackRequest {
    pub feedback: String,
}

#[derive(Deserialize)]
pub struct SbomFilter {
    #[serde(default)]
    pub repo_id: Option<String>,
    #[serde(default)]
    pub package_manager: Option<String>,
    #[serde(default)]
    pub q: Option<String>,
    #[serde(default)]
    pub has_vulns: Option<bool>,
    #[serde(default)]
    pub license: Option<String>,
    #[serde(default = "default_page")]
    pub page: u64,
    #[serde(default = "default_limit")]
    pub limit: i64,
}

#[derive(Deserialize)]
pub struct SbomExportParams {
    pub repo_id: String,
    #[serde(default = "default_export_format")]
    pub format: String,
}

fn default_export_format() -> String {
    "cyclonedx".to_string()
}

#[derive(Deserialize)]
pub struct SbomDiffParams {
    pub repo_a: String,
    pub repo_b: String,
}

#[derive(Serialize)]
pub struct LicenseSummary {
    pub license: String,
    pub count: u64,
    pub is_copyleft: bool,
    pub packages: Vec<String>,
}

#[derive(Serialize)]
pub struct SbomDiffResult {
    pub only_in_a: Vec<SbomDiffEntry>,
    pub only_in_b: Vec<SbomDiffEntry>,
    pub version_changed: Vec<SbomVersionDiff>,
    pub common_count: u64,
}

#[derive(Serialize)]
pub struct SbomDiffEntry {
    pub name: String,
    pub version: String,
    pub package_manager: String,
}

#[derive(Serialize)]
pub struct SbomVersionDiff {
    pub name: String,
    pub package_manager: String,
    pub version_a: String,
    pub version_b: String,
}

pub(crate) type AgentExt = axum::extract::Extension<std::sync::Arc<crate::agent::ComplianceAgent>>;
pub(crate) type ApiResult<T> = Result<axum::Json<ApiResponse<T>>, axum::http::StatusCode>;

/// Resolve a tenant-scoped [`Database`] from the request's
/// [`TenantContext`] (inserted by the M7.1 JWT middleware, or by the
/// dev fallback in unsecured environments). The pool ensures the
/// tenant's indexes idempotently.
///
/// Returns 500 on the rare path where Mongo refuses the database
/// handle — the M7.1 auth/status middleware already rejects every
/// other failure mode with 4xx before we get here.
pub(crate) async fn tenant_db(
    agent: &crate::agent::ComplianceAgent,
    tenant: &compliance_core::tenant_ctx::TenantCtx,
) -> Result<crate::database::Database, axum::http::StatusCode> {
    agent.db_pool.for_tenant(&tenant.0).await.map_err(|e| {
        tracing::error!(
            tenant_id = %tenant.0.tenant_id,
            "Failed to acquire tenant database: {e}"
        );
        axum::http::StatusCode::INTERNAL_SERVER_ERROR
    })
}

pub(crate) async fn collect_cursor_async<T: serde::de::DeserializeOwned + Unpin + Send>(
    mut cursor: mongodb::Cursor<T>,
) -> Vec<T> {
    use futures_util::StreamExt;
    let mut items = Vec::new();
    while let Some(result) = cursor.next().await {
        match result {
            Ok(item) => items.push(item),
            Err(e) => tracing::warn!("Failed to deserialize document: {e}"),
        }
    }
    items
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    // ── PaginationParams ─────────────────────────────────────────

    #[test]
    fn pagination_params_defaults() {
        let p: PaginationParams = serde_json::from_str("{}").unwrap();
        assert_eq!(p.page, 1);
        assert_eq!(p.limit, 50);
    }

    #[test]
    fn pagination_params_custom_values() {
        let p: PaginationParams = serde_json::from_str(r#"{"page":3,"limit":10}"#).unwrap();
        assert_eq!(p.page, 3);
        assert_eq!(p.limit, 10);
    }

    #[test]
    fn pagination_params_partial_override() {
        let p: PaginationParams = serde_json::from_str(r#"{"page":5}"#).unwrap();
        assert_eq!(p.page, 5);
        assert_eq!(p.limit, 50);
    }

    #[test]
    fn pagination_params_zero_page() {
        let p: PaginationParams = serde_json::from_str(r#"{"page":0}"#).unwrap();
        assert_eq!(p.page, 0);
    }

    // ── FindingsFilter ───────────────────────────────────────────

    #[test]
    fn findings_filter_all_defaults() {
        let f: FindingsFilter = serde_json::from_str("{}").unwrap();
        assert!(f.repo_id.is_none());
        assert!(f.severity.is_none());
        assert!(f.scan_type.is_none());
        assert!(f.status.is_none());
        assert!(f.q.is_none());
        assert!(f.sort_by.is_none());
        assert!(f.sort_order.is_none());
        assert_eq!(f.page, 1);
        assert_eq!(f.limit, 50);
    }

    #[test]
    fn findings_filter_with_all_fields() {
        let f: FindingsFilter = serde_json::from_str(
            r#"{
            "repo_id": "abc",
            "severity": "high",
            "scan_type": "sast",
            "status": "open",
            "q": "sql injection",
            "sort_by": "severity",
            "sort_order": "desc",
            "page": 2,
            "limit": 25
        }"#,
        )
        .unwrap();
        assert_eq!(f.repo_id.as_deref(), Some("abc"));
        assert_eq!(f.severity.as_deref(), Some("high"));
        assert_eq!(f.scan_type.as_deref(), Some("sast"));
        assert_eq!(f.status.as_deref(), Some("open"));
        assert_eq!(f.q.as_deref(), Some("sql injection"));
        assert_eq!(f.sort_by.as_deref(), Some("severity"));
        assert_eq!(f.sort_order.as_deref(), Some("desc"));
        assert_eq!(f.page, 2);
        assert_eq!(f.limit, 25);
    }

    #[test]
    fn findings_filter_empty_string_fields() {
        let f: FindingsFilter = serde_json::from_str(r#"{"repo_id":"","severity":""}"#).unwrap();
        assert_eq!(f.repo_id.as_deref(), Some(""));
        assert_eq!(f.severity.as_deref(), Some(""));
    }

    // ── ApiResponse ──────────────────────────────────────────────

    #[test]
    fn api_response_serializes_with_all_fields() {
        let resp = ApiResponse {
            data: vec!["a", "b"],
            total: Some(100),
            page: Some(1),
        };
        let v = serde_json::to_value(&resp).unwrap();
        assert_eq!(v["data"], json!(["a", "b"]));
        assert_eq!(v["total"], 100);
        assert_eq!(v["page"], 1);
    }

    #[test]
    fn api_response_skips_none_fields() {
        let resp = ApiResponse {
            data: "hello",
            total: None,
            page: None,
        };
        let v = serde_json::to_value(&resp).unwrap();
        assert_eq!(v["data"], "hello");
        assert!(v.get("total").is_none());
        assert!(v.get("page").is_none());
    }

    #[test]
    fn api_response_with_nested_struct() {
        #[derive(Serialize)]
        struct Item {
            id: u32,
        }
        let resp = ApiResponse {
            data: Item { id: 42 },
            total: Some(1),
            page: None,
        };
        let v = serde_json::to_value(&resp).unwrap();
        assert_eq!(v["data"]["id"], 42);
        assert_eq!(v["total"], 1);
        assert!(v.get("page").is_none());
    }

    #[test]
    fn api_response_empty_vec() {
        let resp: ApiResponse<Vec<String>> = ApiResponse {
            data: vec![],
            total: Some(0),
            page: Some(1),
        };
        let v = serde_json::to_value(&resp).unwrap();
        assert!(v["data"].as_array().unwrap().is_empty());
    }

    // ── SbomFilter ───────────────────────────────────────────────

    #[test]
    fn sbom_filter_defaults() {
        let f: SbomFilter = serde_json::from_str("{}").unwrap();
        assert!(f.repo_id.is_none());
        assert!(f.package_manager.is_none());
        assert!(f.q.is_none());
        assert!(f.has_vulns.is_none());
        assert!(f.license.is_none());
        assert_eq!(f.page, 1);
        assert_eq!(f.limit, 50);
    }

    #[test]
    fn sbom_filter_has_vulns_bool() {
        let f: SbomFilter = serde_json::from_str(r#"{"has_vulns": true}"#).unwrap();
        assert_eq!(f.has_vulns, Some(true));
    }

    // ── SbomExportParams ─────────────────────────────────────────

    #[test]
    fn sbom_export_params_default_format() {
        let p: SbomExportParams = serde_json::from_str(r#"{"repo_id":"r1"}"#).unwrap();
        assert_eq!(p.repo_id, "r1");
        assert_eq!(p.format, "cyclonedx");
    }

    #[test]
    fn sbom_export_params_custom_format() {
        let p: SbomExportParams =
            serde_json::from_str(r#"{"repo_id":"r1","format":"spdx"}"#).unwrap();
        assert_eq!(p.format, "spdx");
    }

    // ── AddRepositoryRequest ─────────────────────────────────────

    #[test]
    fn add_repository_request_defaults() {
        let r: AddRepositoryRequest = serde_json::from_str(
            r#"{
            "name": "my-repo",
            "git_url": "https://github.com/x/y.git"
        }"#,
        )
        .unwrap();
        assert_eq!(r.name, "my-repo");
        assert_eq!(r.git_url, "https://github.com/x/y.git");
        assert_eq!(r.default_branch, "main");
        assert!(r.auth_token.is_none());
        assert!(r.tracker_type.is_none());
        assert!(r.scan_schedule.is_none());
    }

    #[test]
    fn add_repository_request_custom_branch() {
        let r: AddRepositoryRequest = serde_json::from_str(
            r#"{
            "name": "repo",
            "git_url": "url",
            "default_branch": "develop"
        }"#,
        )
        .unwrap();
        assert_eq!(r.default_branch, "develop");
    }

    // ── UpdateStatusRequest / BulkUpdateStatusRequest ────────────

    #[test]
    fn update_status_request() {
        let r: UpdateStatusRequest = serde_json::from_str(r#"{"status":"resolved"}"#).unwrap();
        assert_eq!(r.status, "resolved");
    }

    #[test]
    fn bulk_update_status_request() {
        let r: BulkUpdateStatusRequest =
            serde_json::from_str(r#"{"ids":["a","b"],"status":"dismissed"}"#).unwrap();
        assert_eq!(r.ids, vec!["a", "b"]);
        assert_eq!(r.status, "dismissed");
    }

    #[test]
    fn bulk_update_status_empty_ids() {
        let r: BulkUpdateStatusRequest =
            serde_json::from_str(r#"{"ids":[],"status":"x"}"#).unwrap();
        assert!(r.ids.is_empty());
    }

    // ── SbomDiffResult serialization ─────────────────────────────

    #[test]
    fn sbom_diff_result_serializes() {
        let r = SbomDiffResult {
            only_in_a: vec![SbomDiffEntry {
                name: "pkg-a".to_string(),
                version: "1.0".to_string(),
                package_manager: "npm".to_string(),
            }],
            only_in_b: vec![],
            version_changed: vec![SbomVersionDiff {
                name: "shared".to_string(),
                package_manager: "cargo".to_string(),
                version_a: "0.1".to_string(),
                version_b: "0.2".to_string(),
            }],
            common_count: 10,
        };
        let v = serde_json::to_value(&r).unwrap();
        assert_eq!(v["only_in_a"].as_array().unwrap().len(), 1);
        assert_eq!(v["only_in_b"].as_array().unwrap().len(), 0);
        assert_eq!(v["version_changed"][0]["version_a"], "0.1");
        assert_eq!(v["common_count"], 10);
    }

    // ── LicenseSummary ───────────────────────────────────────────

    #[test]
    fn license_summary_serializes() {
        let ls = LicenseSummary {
            license: "MIT".to_string(),
            count: 42,
            is_copyleft: false,
            packages: vec!["serde".to_string()],
        };
        let v = serde_json::to_value(&ls).unwrap();
        assert_eq!(v["license"], "MIT");
        assert_eq!(v["is_copyleft"], false);
        assert_eq!(v["count"], 42);
    }

    // ── Default helper functions ─────────────────────────────────

    #[test]
    fn default_page_returns_1() {
        assert_eq!(default_page(), 1);
    }

    #[test]
    fn default_limit_returns_50() {
        assert_eq!(default_limit(), 50);
    }
}
