//! Conversion between [`ModuleInfo`](crate::ModuleInfo) and [`CachedModule`].
//!
//! Both functions convert between borrowed source structs and owned target structs
//! (`&CachedModule -> ModuleInfo`, `&ModuleInfo -> CachedModule`). All `String` clones
//! are structurally necessary: the cache store retains ownership of `CachedModule`
//! entries (for persistence), and `ModuleInfo` must outlive the cache for the
//! analysis pipeline. Eliminating these clones would require shared ownership
//! (`Arc<str>`) across the entire extraction + analysis pipeline.

use std::time::{SystemTime, UNIX_EPOCH};

use oxc_span::Span;

use crate::ExportName;
use fallow_types::extract::{NamespaceObjectAlias, VisibilityTag};

/// Seconds-since-Unix-epoch from the wall clock, saturating to 0 if the
/// system clock is set before the epoch. Used as the LRU bookkeeping
/// timestamp on `CachedModule.last_access_secs`. Wall-clock (not monotonic)
/// is the right source here because the value persists across process
/// invocations.
#[must_use]
pub fn current_unix_seconds() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_or(0, |d| d.as_secs())
}

use super::types::{
    CachedDynamicImport, CachedDynamicImportPattern, CachedExport, CachedImport,
    CachedLocalTypeDeclaration, CachedMember, CachedModule, CachedNamespaceObjectAlias,
    CachedPublicSignatureTypeReference, CachedReExport, CachedRequireCall, CachedSuppression,
    CachedUnknownSuppressionKind, IMPORT_KIND_DEFAULT, IMPORT_KIND_NAMED, IMPORT_KIND_NAMESPACE,
    IMPORT_KIND_SIDE_EFFECT,
};

/// Reconstruct a [`ModuleInfo`](crate::ModuleInfo) from a [`CachedModule`].
#[must_use]
pub fn cached_to_module(
    cached: &CachedModule,
    file_id: fallow_types::discover::FileId,
) -> crate::ModuleInfo {
    cached_to_module_opts(cached, file_id, true)
}

/// Reconstruct a [`ModuleInfo`](crate::ModuleInfo) from a [`CachedModule`], skipping
/// the per-function complexity vec when `need_complexity` is `false`. Avoids the
/// `Vec<FunctionComplexity>` clone on warm runs of commands (e.g. `fallow dead-code`)
/// that don't consume complexity, which adds up across tens of thousands of files.
#[must_use]
#[expect(
    clippy::too_many_lines,
    reason = "single flat field-by-field deserialization; splitting it harms readability"
)]
pub fn cached_to_module_opts(
    cached: &CachedModule,
    file_id: fallow_types::discover::FileId,
    need_complexity: bool,
) -> crate::ModuleInfo {
    use crate::{
        DynamicImportInfo, ExportInfo, ImportInfo, ImportedName, LocalTypeDeclaration, MemberInfo,
        ModuleInfo, PublicSignatureTypeReference, ReExportInfo, RequireCallInfo,
    };

    let exports = cached
        .exports
        .iter()
        .map(|e| ExportInfo {
            name: if e.is_default {
                ExportName::Default
            } else {
                ExportName::Named(e.name.clone())
            },
            local_name: e.local_name.clone(),
            is_type_only: e.is_type_only,
            is_side_effect_used: e.is_side_effect_used,
            visibility: match e.visibility {
                1 => VisibilityTag::Public,
                2 => VisibilityTag::Internal,
                3 => VisibilityTag::Beta,
                4 => VisibilityTag::Alpha,
                5 => VisibilityTag::ExpectedUnused,
                _ => VisibilityTag::None,
            },
            span: Span::new(e.span_start, e.span_end),
            members: e
                .members
                .iter()
                .map(|m| MemberInfo {
                    name: m.name.clone(),
                    kind: m.kind,
                    span: Span::new(m.span_start, m.span_end),
                    has_decorator: m.has_decorator,
                    decorator_names: m.decorator_names.clone(),
                    is_instance_returning_static: m.is_instance_returning_static,
                    is_self_returning: m.is_self_returning,
                })
                .collect(),
            super_class: e.super_class.clone(),
        })
        .collect();

    let imports = cached
        .imports
        .iter()
        .map(|i| ImportInfo {
            source: i.source.clone(),
            imported_name: match i.kind {
                IMPORT_KIND_DEFAULT => ImportedName::Default,
                IMPORT_KIND_NAMESPACE => ImportedName::Namespace,
                IMPORT_KIND_SIDE_EFFECT => ImportedName::SideEffect,
                // IMPORT_KIND_NAMED (0) and any unknown value default to Named
                _ => ImportedName::Named(i.imported_name.clone()),
            },
            local_name: i.local_name.clone(),
            is_type_only: i.is_type_only,
            from_style: i.from_style,
            span: Span::new(i.span_start, i.span_end),
            source_span: Span::new(i.source_span_start, i.source_span_end),
        })
        .collect();

    let re_exports = cached
        .re_exports
        .iter()
        .map(|r| ReExportInfo {
            source: r.source.clone(),
            imported_name: r.imported_name.clone(),
            exported_name: r.exported_name.clone(),
            is_type_only: r.is_type_only,
            span: Span::new(r.span_start, r.span_end),
        })
        .collect();

    let dynamic_imports = cached
        .dynamic_imports
        .iter()
        .map(|d| DynamicImportInfo {
            source: d.source.clone(),
            span: Span::new(d.span_start, d.span_end),
            destructured_names: d.destructured_names.clone(),
            local_name: d.local_name.clone(),
            is_speculative: d.is_speculative,
        })
        .collect();

    let require_calls = cached
        .require_calls
        .iter()
        .map(|r| RequireCallInfo {
            source: r.source.clone(),
            span: Span::new(r.span_start, r.span_end),
            source_span: Span::new(r.source_span_start, r.source_span_end),
            destructured_names: r.destructured_names.clone(),
            local_name: r.local_name.clone(),
        })
        .collect();

    let dynamic_import_patterns = cached
        .dynamic_import_patterns
        .iter()
        .map(|p| crate::DynamicImportPattern {
            prefix: p.prefix.clone(),
            suffix: p.suffix.clone(),
            span: Span::new(p.span_start, p.span_end),
        })
        .collect();

    let suppressions = cached
        .suppressions
        .iter()
        .map(|s| crate::suppress::Suppression {
            line: s.line,
            comment_line: s.comment_line,
            kind: if s.kind == 0 {
                None
            } else {
                crate::suppress::IssueKind::from_discriminant(s.kind)
            },
        })
        .collect();

    let unknown_suppression_kinds = cached
        .unknown_suppression_kinds
        .iter()
        .map(|u| fallow_types::suppress::UnknownSuppressionKind {
            comment_line: u.comment_line,
            is_file_level: u.is_file_level,
            token: u.token.clone(),
        })
        .collect();

    ModuleInfo {
        file_id,
        exports,
        imports,
        re_exports,
        dynamic_imports,
        dynamic_import_patterns,
        require_calls,
        package_path_references: cached.package_path_references.clone(),
        member_accesses: cached.member_accesses.clone(),
        whole_object_uses: cached.whole_object_uses.clone(),
        has_cjs_exports: cached.has_cjs_exports,
        has_angular_component_template_url: cached.has_angular_component_template_url,
        content_hash: cached.content_hash,
        suppressions,
        unknown_suppression_kinds,
        unused_import_bindings: cached.unused_import_bindings.clone(),
        type_referenced_import_bindings: cached.type_referenced_import_bindings.clone(),
        value_referenced_import_bindings: cached.value_referenced_import_bindings.clone(),
        line_offsets: cached.line_offsets.clone(),
        complexity: if need_complexity {
            cached.complexity.clone()
        } else {
            Vec::new()
        },
        flag_uses: cached.flag_uses.clone(),
        class_heritage: cached.class_heritage.clone(),
        injection_tokens: cached.injection_tokens.clone(),
        local_type_declarations: cached
            .local_type_declarations
            .iter()
            .map(|decl| LocalTypeDeclaration {
                name: decl.name.clone(),
                span: Span::new(decl.span_start, decl.span_end),
            })
            .collect(),
        public_signature_type_references: cached
            .public_signature_type_references
            .iter()
            .map(|reference| PublicSignatureTypeReference {
                export_name: reference.export_name.clone(),
                type_name: reference.type_name.clone(),
                span: Span::new(reference.span_start, reference.span_end),
            })
            .collect(),
        namespace_object_aliases: cached
            .namespace_object_aliases
            .iter()
            .map(|alias| NamespaceObjectAlias {
                via_export_name: alias.via_export_name.clone(),
                suffix: alias.suffix.clone(),
                namespace_local: alias.namespace_local.clone(),
            })
            .collect(),
        iconify_prefixes: cached.iconify_prefixes.clone(),
        iconify_icon_names: cached.iconify_icon_names.clone(),
        auto_import_candidates: cached.auto_import_candidates.clone(),
        directives: cached.directives.clone(),
        security_sinks: cached.security_sinks.clone(),
        security_sinks_skipped: cached.security_sinks_skipped,
        tainted_bindings: cached.tainted_bindings.clone(),
        sanitized_sink_args: cached.sanitized_sink_args.clone(),
        security_control_sites: cached.security_control_sites.clone(),
    }
}

/// Convert a [`ModuleInfo`](crate::ModuleInfo) to a [`CachedModule`] for storage.
///
/// `mtime_secs` and `file_size` come from `std::fs::metadata()` at parse time
/// and enable fast cache validation on subsequent runs (skip file read when
/// mtime+size match).
#[must_use]
#[expect(
    clippy::too_many_lines,
    reason = "single flat field-by-field serialization; splitting it harms readability"
)]
pub fn module_to_cached(
    module: &crate::ModuleInfo,
    mtime_secs: u64,
    file_size: u64,
) -> CachedModule {
    CachedModule {
        content_hash: module.content_hash,
        mtime_secs,
        file_size,
        last_access_secs: current_unix_seconds(),
        exports: module
            .exports
            .iter()
            .map(|e| CachedExport {
                name: match &e.name {
                    ExportName::Named(n) => n.clone(),
                    ExportName::Default => String::new(),
                },
                is_default: matches!(e.name, ExportName::Default),
                is_type_only: e.is_type_only,
                is_side_effect_used: e.is_side_effect_used,
                visibility: e.visibility as u8,
                local_name: e.local_name.clone(),
                span_start: e.span.start,
                span_end: e.span.end,
                members: e
                    .members
                    .iter()
                    .map(|m| CachedMember {
                        name: m.name.clone(),
                        kind: m.kind,
                        span_start: m.span.start,
                        span_end: m.span.end,
                        has_decorator: m.has_decorator,
                        decorator_names: m.decorator_names.clone(),
                        is_instance_returning_static: m.is_instance_returning_static,
                        is_self_returning: m.is_self_returning,
                    })
                    .collect(),
                super_class: e.super_class.clone(),
            })
            .collect(),
        imports: module
            .imports
            .iter()
            .map(|i| {
                let (kind, imported_name) = match &i.imported_name {
                    crate::ImportedName::Named(n) => (IMPORT_KIND_NAMED, n.clone()),
                    crate::ImportedName::Default => (IMPORT_KIND_DEFAULT, String::new()),
                    crate::ImportedName::Namespace => (IMPORT_KIND_NAMESPACE, String::new()),
                    crate::ImportedName::SideEffect => (IMPORT_KIND_SIDE_EFFECT, String::new()),
                };
                CachedImport {
                    source: i.source.clone(),
                    imported_name,
                    local_name: i.local_name.clone(),
                    is_type_only: i.is_type_only,
                    from_style: i.from_style,
                    kind,
                    span_start: i.span.start,
                    span_end: i.span.end,
                    source_span_start: i.source_span.start,
                    source_span_end: i.source_span.end,
                }
            })
            .collect(),
        re_exports: module
            .re_exports
            .iter()
            .map(|r| CachedReExport {
                source: r.source.clone(),
                imported_name: r.imported_name.clone(),
                exported_name: r.exported_name.clone(),
                is_type_only: r.is_type_only,
                span_start: r.span.start,
                span_end: r.span.end,
            })
            .collect(),
        dynamic_imports: module
            .dynamic_imports
            .iter()
            .map(|d| CachedDynamicImport {
                source: d.source.clone(),
                span_start: d.span.start,
                span_end: d.span.end,
                destructured_names: d.destructured_names.clone(),
                local_name: d.local_name.clone(),
                is_speculative: d.is_speculative,
            })
            .collect(),
        require_calls: module
            .require_calls
            .iter()
            .map(|r| CachedRequireCall {
                source: r.source.clone(),
                span_start: r.span.start,
                span_end: r.span.end,
                source_span_start: r.source_span.start,
                source_span_end: r.source_span.end,
                destructured_names: r.destructured_names.clone(),
                local_name: r.local_name.clone(),
            })
            .collect(),
        package_path_references: module.package_path_references.clone(),
        member_accesses: module.member_accesses.clone(),
        whole_object_uses: module.whole_object_uses.clone(),
        dynamic_import_patterns: module
            .dynamic_import_patterns
            .iter()
            .map(|p| CachedDynamicImportPattern {
                prefix: p.prefix.clone(),
                suffix: p.suffix.clone(),
                span_start: p.span.start,
                span_end: p.span.end,
            })
            .collect(),
        has_cjs_exports: module.has_cjs_exports,
        has_angular_component_template_url: module.has_angular_component_template_url,
        unused_import_bindings: module.unused_import_bindings.clone(),
        type_referenced_import_bindings: module.type_referenced_import_bindings.clone(),
        value_referenced_import_bindings: module.value_referenced_import_bindings.clone(),
        suppressions: module
            .suppressions
            .iter()
            .map(|s| CachedSuppression {
                line: s.line,
                comment_line: s.comment_line,
                kind: s
                    .kind
                    .map_or(0, crate::suppress::IssueKind::to_discriminant),
            })
            .collect(),
        unknown_suppression_kinds: module
            .unknown_suppression_kinds
            .iter()
            .map(|u| CachedUnknownSuppressionKind {
                comment_line: u.comment_line,
                is_file_level: u.is_file_level,
                token: u.token.clone(),
            })
            .collect(),
        line_offsets: module.line_offsets.clone(),
        complexity: module.complexity.clone(),
        flag_uses: module.flag_uses.clone(),
        class_heritage: module.class_heritage.clone(),
        injection_tokens: module.injection_tokens.clone(),
        local_type_declarations: module
            .local_type_declarations
            .iter()
            .map(|decl| CachedLocalTypeDeclaration {
                name: decl.name.clone(),
                span_start: decl.span.start,
                span_end: decl.span.end,
            })
            .collect(),
        public_signature_type_references: module
            .public_signature_type_references
            .iter()
            .map(|reference| CachedPublicSignatureTypeReference {
                export_name: reference.export_name.clone(),
                type_name: reference.type_name.clone(),
                span_start: reference.span.start,
                span_end: reference.span.end,
            })
            .collect(),
        namespace_object_aliases: module
            .namespace_object_aliases
            .iter()
            .map(|alias| CachedNamespaceObjectAlias {
                via_export_name: alias.via_export_name.clone(),
                suffix: alias.suffix.clone(),
                namespace_local: alias.namespace_local.clone(),
            })
            .collect(),
        iconify_prefixes: module.iconify_prefixes.clone(),
        iconify_icon_names: module.iconify_icon_names.clone(),
        auto_import_candidates: module.auto_import_candidates.clone(),
        directives: module.directives.clone(),
        security_sinks: module.security_sinks.clone(),
        security_sinks_skipped: module.security_sinks_skipped,
        tainted_bindings: module.tainted_bindings.clone(),
        sanitized_sink_args: module.sanitized_sink_args.clone(),
        security_control_sites: module.security_control_sites.clone(),
    }
}
