//! Core scanner logic for conflict detection and observation recording.

use std::collections::HashSet;
use std::path::Path;
use std::time::Instant;

use tracing::{info, instrument};

use crate::bridge::{self, claim_to_observation};
use crate::config::{AphoriaConfig, SyncMode};
use crate::episteme::{
    create_authoritative_corpus, current_timestamp_millis, ConceptIndex, EphemeralDetector,
    LocalEpisteme,
};
use crate::error::AphoriaError;
use crate::hosted::HostedClient;
use crate::policy::PolicyManager;
use crate::types::{
    ConflictResult, DriftResult, ExtractedClaim, FileSource, ScanArgs, ScanMode, ScanResult,
    ScanTiming,
};
use crate::walker::{walk_project, walk_staged_files};

use super::walker::extract_claims_from_files;

/// Result of conflict checking including observation count and drift detection.
pub(super) struct ConflictCheckResult {
    pub conflicts: Vec<ConflictResult>,
    pub drifts: Vec<DriftResult>,
    pub observations_recorded: usize,
}

/// Run a scan on the specified project.
///
/// This is the main entry point for scanning a codebase. It:
/// 1. Walks the project directory
/// 2. Extracts claims from config and code
/// 3. Checks for conflicts against authoritative sources
/// 4. (Optional) Persists claims to Episteme storage if `mode == Persistent`
/// 5. (Optional) Records observations for claims with no conflicts if `sync == true`
/// 6. Returns a formatted report
///
/// # Scan Modes
///
/// - **Ephemeral** (default): Fast in-memory scan. No disk I/O for Episteme.
///   Uses `EphemeralDetector` for conflict detection. Does not support
///   diff/baseline features or observation write-back.
///
/// - **Persistent**: Full scan with Episteme storage. Enables diff, baseline,
///   alias creation, and observation write-back (when `--sync` is enabled).
#[instrument(skip(config), fields(path = %args.path.display(), format = %args.format, mode = ?args.mode, sync = args.sync, file_source = ?args.file_source, benchmark = args.benchmark))]
pub async fn run_scan(args: ScanArgs, config: &AphoriaConfig) -> Result<ScanResult, AphoriaError> {
    info!("Starting scan");

    let total_start = Instant::now();
    let project_root = args.path.canonicalize().unwrap_or_else(|_| args.path.clone());

    // 1. Walk the project to find files (or just staged files)
    let walk_start = Instant::now();
    let files = match args.file_source {
        FileSource::All => walk_project(&project_root, config)?,
        FileSource::Staged => walk_staged_files(&project_root, config)?,
    };
    let walk_ms = walk_start.elapsed().as_millis() as u64;
    info!(files_found = files.len(), file_source = ?args.file_source, walk_ms, "Project walk complete");

    // 2. Extract claims from files (LLM extraction only in persistent mode)
    let extraction_start = Instant::now();
    let all_claims = extract_claims_from_files(&files, config, args.mode, &project_root)?;
    let extraction_ms = extraction_start.elapsed().as_millis() as u64;
    info!(claims_extracted = all_claims.len(), extraction_ms, "Extraction complete");

    // 3. Check for conflicts - mode determines which path
    let conflict_start = Instant::now();
    let result = check_conflicts(&args, &all_claims, &project_root, config).await?;
    let conflict_ms = conflict_start.elapsed().as_millis() as u64;

    let total_ms = total_start.elapsed().as_millis() as u64;

    // 4. Calculate lines of code if benchmark mode
    let lines_of_code = if args.benchmark { Some(count_lines_of_code(&files)) } else { None };

    // 5. Build timing info if benchmark mode
    let timing = if args.benchmark {
        Some(ScanTiming { walk_ms, extraction_ms, conflict_ms, total_ms, lines_of_code })
    } else {
        None
    };

    // 6. Populate claims if requested (clone and sort by file, then line)
    let claims = if args.show_claims {
        let mut sorted = all_claims.to_vec();
        sorted.sort_by(|a, b| a.file.cmp(&b.file).then(a.line.cmp(&b.line)));
        Some(sorted)
    } else {
        None
    };

    // 7. Build result
    let project_name =
        project_root.file_name().and_then(|s| s.to_str()).unwrap_or("unknown").to_string();

    Ok(ScanResult {
        project: project_name,
        scan_id: generate_scan_id(),
        files_scanned: files.len(),
        claims_extracted: all_claims.len(),
        conflicts: result.conflicts,
        drifts: result.drifts,
        format: args.format.clone(),
        debug: args.debug,
        observations_recorded: result.observations_recorded,
        timing,
        claims,
        deprecated_usages: vec![], // TODO: Populate from lifecycle store during scan
    })
}

/// Count lines of code in the scanned files.
///
/// Reads each file and counts non-empty lines. Used for benchmark reporting.
fn count_lines_of_code(files: &[crate::walker::WalkedFile]) -> usize {
    files
        .iter()
        .map(|file| {
            std::fs::read_to_string(&file.path)
                .map(|content| content.lines().filter(|line| !line.trim().is_empty()).count())
                .unwrap_or(0)
        })
        .sum()
}

/// Check claims for conflicts using either ephemeral or persistent mode.
async fn check_conflicts(
    args: &ScanArgs,
    all_claims: &[ExtractedClaim],
    project_root: &Path,
    config: &AphoriaConfig,
) -> Result<ConflictCheckResult, AphoriaError> {
    match args.mode {
        ScanMode::Ephemeral => {
            let conflicts =
                check_conflicts_ephemeral(all_claims, project_root, config, args.debug)?;
            // Ephemeral mode never records observations or detects drift (intentionally stateless)
            Ok(ConflictCheckResult { conflicts, drifts: vec![], observations_recorded: 0 })
        }
        ScanMode::Persistent => {
            check_conflicts_persistent(all_claims, project_root, config, args.sync).await
        }
    }
}

/// Fast in-memory conflict detection (no persistence).
fn check_conflicts_ephemeral(
    all_claims: &[ExtractedClaim],
    project_root: &Path,
    config: &AphoriaConfig,
    debug: bool,
) -> Result<Vec<ConflictResult>, AphoriaError> {
    info!("Using ephemeral detector (no persistence)");
    let signing_key = bridge::load_or_generate_key(project_root)?;

    // Load policies if any
    let policy_manager = PolicyManager::new(&config.corpus.cache_dir);
    let policies = policy_manager.load_policies(&config.policies)?;

    // Create detector with policies
    let mut detector = EphemeralDetector::new(&signing_key, &config.corpus);
    detector.ingest_policies(&policies);

    if debug {
        Ok(detector.check_conflicts_debug(all_claims, config))
    } else {
        Ok(detector.check_conflicts(all_claims, config))
    }
}

/// Full conflict detection with Episteme persistence.
///
/// When `sync` is enabled, claims with no authority conflict are written back
/// as Tier 4 (Community) observations, creating "project memory".
///
/// Drift detection runs AFTER authority conflict detection: claims that have
/// no authority conflict are checked against prior observations to detect
/// value changes.
///
/// # Hosted Mode
///
/// When `[hosted]` is configured with a URL, sync is automatically enabled
/// and observations are pushed to the remote server. The `sync_mode` setting
/// controls whether local storage is also used:
///
/// - `remote-only`: Only push to remote (no local storage)
/// - `local-and-remote`: Store locally AND push to remote
async fn check_conflicts_persistent(
    all_claims: &[ExtractedClaim],
    project_root: &Path,
    config: &AphoriaConfig,
    sync: bool,
) -> Result<ConflictCheckResult, AphoriaError> {
    // Auto-enable sync when hosted mode is configured
    let effective_sync = sync || config.hosted.is_enabled();
    let hosted_enabled = config.hosted.is_enabled();

    info!(
        sync = effective_sync,
        hosted = hosted_enabled,
        "Using persistent mode (with Episteme storage)"
    );

    // Open local Episteme and ingest claims
    let mut episteme = LocalEpisteme::open(config, project_root).await?;

    if !all_claims.is_empty() {
        episteme.ingest_claims(all_claims).await?;
    }

    // Build authoritative corpus from bundled sources AND imported Trust Packs
    // This uses LocalEpisteme's check_conflicts which also creates aliases
    let signing_key = bridge::load_or_generate_key(project_root)?;
    let mut corpus = create_authoritative_corpus(&signing_key);

    // Include assertions imported from Trust Packs
    let imported_assertions = episteme.fetch_authoritative_assertions().await?;
    if !imported_assertions.is_empty() {
        info!(
            count = imported_assertions.len(),
            "Including imported Trust Pack assertions in conflict detection"
        );
        corpus.extend(imported_assertions);
    }

    // Merge predicate aliases from config AND from persisted/imported Trust Packs
    // This ensures both config-defined and pack-imported aliases are used for
    // semantic predicate matching (Phase 6.5.3)
    let mut all_predicate_aliases = config.predicate_aliases.to_alias_sets();
    all_predicate_aliases.extend(episteme.predicate_aliases().iter().cloned());

    if !all_predicate_aliases.is_empty() {
        info!(
            config_count = config.predicate_aliases.to_alias_sets().len(),
            stored_count = episteme.predicate_aliases().len(),
            total_count = all_predicate_aliases.len(),
            "Using predicate aliases for index normalization"
        );
    }

    // Build index WITH predicate alias normalization so both authority and code
    // predicates use canonical forms (e.g., "required" normalizes to "enabled")
    let index = ConceptIndex::build_with_aliases(&corpus, &all_predicate_aliases);
    let conflicts = episteme.check_conflicts(all_claims, config, &index).await?;

    // Find claims that DO have an authority conflict
    let conflicting_paths: HashSet<_> =
        conflicts.iter().map(|c| c.claim.concept_path.as_str()).collect();

    // Non-conflicting claims are candidates for drift detection and observation write-back
    let non_conflicting_claims: Vec<_> = all_claims
        .iter()
        .filter(|c| !conflicting_paths.contains(c.concept_path.as_str()))
        .cloned()
        .collect();

    // Drift detection: check non-conflicting claims against prior observations
    let drifts = episteme.check_drift(&non_conflicting_claims).await?;

    // Find claims that drifted (we don't want to overwrite them with new observations)
    let drifting_paths: HashSet<_> = drifts.iter().map(|d| d.claim.concept_path.as_str()).collect();

    // Write observations for novel claims (no conflict AND no drift) if sync enabled
    let observations_recorded = if effective_sync {
        // Novel claims are those with NO authority conflict AND NO drift
        let novel_claims: Vec<_> = non_conflicting_claims
            .iter()
            .filter(|c| !drifting_paths.contains(c.concept_path.as_str()))
            .cloned()
            .collect();

        let mut local_count = 0;
        let mut remote_count = 0;

        // Local persistence (unless hosted mode is remote-only without fallback)
        let should_persist_locally =
            !hosted_enabled || config.hosted.sync_mode == SyncMode::LocalAndRemote;

        if should_persist_locally && !novel_claims.is_empty() {
            local_count = episteme.ingest_observations(&novel_claims).await?;
            info!(count = local_count, "Recorded observations locally");
        }

        // Remote push (if hosted mode is enabled)
        if hosted_enabled && !novel_claims.is_empty() {
            // Get project name for fallback
            let project_name =
                project_root.file_name().and_then(|s| s.to_str()).unwrap_or("unknown");

            // Create hosted client
            if let Some(client) = HostedClient::new(&config.hosted, &signing_key, project_name)? {
                // Convert claims to observations
                let timestamp = std::time::SystemTime::now()
                    .duration_since(std::time::UNIX_EPOCH)
                    .map(|d| d.as_secs())
                    .unwrap_or(0);

                let observations: Vec<_> = novel_claims
                    .iter()
                    .map(|c| claim_to_observation(c, &signing_key, timestamp))
                    .collect();

                remote_count = client.push_observations(observations)?;
                info!(count = remote_count, "Pushed observations to hosted server");
            }
        }

        // Return the higher count (they should be the same for LocalAndRemote)
        local_count.max(remote_count)
    } else {
        0
    };

    // Shut down Episteme
    episteme.shutdown().await;

    Ok(ConflictCheckResult { conflicts, drifts, observations_recorded })
}

/// Generate a unique scan ID.
pub fn generate_scan_id() -> String {
    format!("scan-{}", current_timestamp_millis())
}

/// Extract claims from a project without running conflict detection.
///
/// This is used for community preview to show what observations would be shared.
/// Note: LLM extraction is not used for preview (uses ScanMode::Ephemeral).
#[instrument(skip(config), fields(path = %args.path.display(), file_source = ?args.file_source))]
pub async fn extract_claims(
    args: &ScanArgs,
    config: &AphoriaConfig,
) -> Result<Vec<ExtractedClaim>, AphoriaError> {
    info!("Extracting claims for preview");

    let project_root = args.path.canonicalize().unwrap_or_else(|_| args.path.clone());

    // Walk the project to find files
    let files = match args.file_source {
        FileSource::All => walk_project(&project_root, config)?,
        FileSource::Staged => walk_staged_files(&project_root, config)?,
    };
    info!(files_found = files.len(), "Project walk complete");

    // Extract claims from files (ephemeral mode - no LLM)
    let claims = extract_claims_from_files(&files, config, ScanMode::Ephemeral, &project_root)?;
    info!(claims_extracted = claims.len(), "Extraction complete");

    Ok(claims)
}
