//! Profile commands for multi-run stability analysis (Phase 3)
//!
//! # Usage
//! ```bash
//! assay profile init --output profile.yaml --name my-app
//! assay profile update --profile profile.yaml -i trace.jsonl --run-id ci-123
//! assay profile show --profile profile.yaml
//! ```

use anyhow::{Context, Result};
use clap::{Args, Subcommand};
use serde::Serialize;
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::time::Instant;

use super::pipeline_error::elapsed_ms;
use super::profile_types::*;

// ─────────────────────────────────────────────────────────────────────────────
// CLI Args
// ─────────────────────────────────────────────────────────────────────────────

#[derive(Args, Debug, Clone)]
#[command(about = "Manage multi-run profiles for stability analysis")]
pub struct ProfileArgs {
    #[command(subcommand)]
    pub cmd: ProfileCmd,
}

#[derive(Subcommand, Debug, Clone)]
pub enum ProfileCmd {
    /// Initialize a new profile
    Init(InitArgs),
    /// Update profile with a new run
    Update(UpdateArgs),
    /// Show profile summary
    Show(ShowArgs),
}

#[derive(Args, Debug, Clone)]
pub struct InitArgs {
    #[arg(short, long, default_value = "assay-profile.yaml")]
    pub output: PathBuf,

    #[arg(long, default_value = "default")]
    pub name: String,

    /// Scope fingerprint (config hash, suite name)
    #[arg(long)]
    pub scope: Option<String>,
}

#[derive(Args, Debug, Clone)]
pub struct UpdateArgs {
    #[arg(long)]
    pub profile: PathBuf,

    #[arg(short, long)]
    pub input: PathBuf,

    /// Idempotency key (required) - e.g. CI run id
    #[arg(long)]
    pub run_id: String,

    /// Fail if run_id already merged
    #[arg(long)]
    pub strict: bool,

    /// Scope fingerprint check (prevents pollution)
    #[arg(long)]
    pub scope: Option<String>,

    /// Force update even if scope mismatch
    #[arg(long)]
    pub force: bool,

    /// Verbose output
    #[arg(short, long)]
    pub verbose: bool,
}

#[derive(Args, Debug, Clone)]
pub struct ShowArgs {
    #[arg(long)]
    pub profile: PathBuf,

    /// Output format: summary, yaml, json
    #[arg(long, default_value = "summary")]
    pub format: String,

    /// Show top N entries per category
    #[arg(long, default_value_t = 10)]
    pub top: usize,
}

// ─────────────────────────────────────────────────────────────────────────────
// Event Types (reuse from generate.rs or define here)
// ─────────────────────────────────────────────────────────────────────────────

use serde::Deserialize;

#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Event {
    FileOpen {
        path: String,
        #[serde(default)]
        timestamp: u64,
    },
    NetConnect {
        dest: String,
        #[serde(default)]
        timestamp: u64,
    },
    ProcExec {
        path: String,
        #[serde(default)]
        timestamp: u64,
    },
}

fn read_events(path: &PathBuf) -> Result<Vec<Event>> {
    use std::io::{BufRead, BufReader};

    let reader: Box<dyn BufRead> = if path.to_string_lossy() == "-" {
        Box::new(BufReader::new(std::io::stdin()))
    } else {
        Box::new(BufReader::new(std::fs::File::open(path)?))
    };

    let mut events = Vec::new();
    for line in reader.lines() {
        let line = line?;
        if line.trim().is_empty() || line.starts_with('#') {
            continue;
        }
        if let Ok(e) = serde_json::from_str(&line) {
            events.push(e);
        }
    }
    Ok(events)
}

// ─────────────────────────────────────────────────────────────────────────────
// Commands
// ─────────────────────────────────────────────────────────────────────────────

pub fn run(args: ProfileArgs) -> Result<i32> {
    match args.cmd {
        ProfileCmd::Init(a) => cmd_init(a),
        ProfileCmd::Update(a) => cmd_update(a),
        ProfileCmd::Show(a) => cmd_show(a),
    }
}

#[derive(Debug, Serialize)]
struct ProfilePerfMetrics {
    load_profile_ms: u64,
    read_events_ms: u64,
    aggregate_ms: u64,
    merge_ms: u64,
    save_profile_ms: u64,
    profile_store_ms: u64,
    total_ms: u64,
    run_entries: usize,
    run_id_window_len: usize,
    run_id_digest_window_len: usize,
    run_id_memory_bytes: u64,
}

fn cmd_init(args: InitArgs) -> Result<i32> {
    if args.output.exists() {
        anyhow::bail!("profile already exists: {}", args.output.display());
    }

    let profile = Profile::new(&args.name, args.scope);
    save_profile(&profile, &args.output)?;

    eprintln!("Created profile: {}", args.output.display());
    Ok(0)
}

fn enforce_scope(profile: &mut Profile, new_scope: Option<&String>, force: bool) -> Result<()> {
    // Hard Scope Guard (SOTA: prevent pollution from different configs)
    if let Some(ref current_scope) = profile.scope {
        if let Some(scope) = new_scope {
            if current_scope != scope {
                if force {
                    eprintln!(
                        "WARNING: Scope mismatch (profile='{}', update='{}'). Forcing update.",
                        current_scope, scope
                    );
                } else {
                    anyhow::bail!(
                        "Scope mismatch: profile scope is '{}' but update scope is '{}'. \
                        This prevents accidentally merging runs from different configurations. \
                        Use --force to override.",
                        current_scope,
                        scope
                    );
                }
            }
        }
    } else if let Some(scope) = new_scope {
        // First time seeing a scope -> lock it
        eprintln!("Setting profile scope to '{}'", scope);
        profile.scope = Some(scope.clone());
    }
    Ok(())
}

fn cmd_update(args: UpdateArgs) -> Result<i32> {
    let total_start = Instant::now();

    // Load existing profile
    let load_start = Instant::now();
    let mut profile = load_profile(&args.profile)
        .with_context(|| format!("failed to load profile: {}", args.profile.display()))?;
    let load_profile_ms = elapsed_ms(load_start);

    // Enforce scope guard
    enforce_scope(&mut profile, args.scope.as_ref(), args.force)?;

    // Idempotency check
    if profile.has_run(&args.run_id) {
        if args.strict {
            anyhow::bail!("run_id '{}' already merged (strict mode)", args.run_id);
        }
        eprintln!("Skipping: run_id '{}' already merged", args.run_id);
        return Ok(0);
    }

    // Read events
    let read_start = Instant::now();
    let events = read_events(&args.input)?;
    let read_events_ms = elapsed_ms(read_start);
    if events.is_empty() {
        eprintln!("Warning: no events in input");
    }

    // Aggregate this run (deduplicated per artifact)
    let aggregate_start = Instant::now();
    let run_data = aggregate_run(&events);
    let aggregate_ms = elapsed_ms(aggregate_start);
    let run_entries = run_data.files.len() + run_data.network.len() + run_data.processes.len();

    if args.verbose {
        eprintln!(
            "Run {}: {} files, {} network, {} processes",
            args.run_id,
            run_data.files.len(),
            run_data.network.len(),
            run_data.processes.len()
        );
    }

    // Merge into profile
    let merge_start = Instant::now();
    let (new_count, updated_count) = merge_run(&mut profile, &run_data);
    let merge_ms = elapsed_ms(merge_start);

    // Update metadata
    profile.total_runs += 1;
    let run_id_digest_evicted = profile.add_run_id(args.run_id.clone());
    profile.updated_at = chrono::Utc::now().to_rfc3339();

    // Save
    let save_start = Instant::now();
    save_profile(&profile, &args.profile)?;
    let save_profile_ms = elapsed_ms(save_start);

    let profile_store_ms = load_profile_ms
        .saturating_add(merge_ms)
        .saturating_add(save_profile_ms);
    let total_ms = elapsed_ms(total_start);
    let perf = ProfilePerfMetrics {
        load_profile_ms,
        read_events_ms,
        aggregate_ms,
        merge_ms,
        save_profile_ms,
        profile_store_ms,
        total_ms,
        run_entries,
        run_id_window_len: profile.run_ids.len(),
        run_id_digest_window_len: profile.run_id_digests.len(),
        run_id_memory_bytes: profile.run_id_memory_bytes_estimate(),
    };

    eprintln!(
        "Updated profile: {} total runs, {} new entries, {} updated",
        profile.total_runs, new_count, updated_count
    );

    if args.verbose || profile_store_ms >= 500 {
        eprintln!(
            "profile-perf: load={}ms read={}ms aggregate={}ms merge={}ms save={}ms store={}ms total={}ms entries={}",
            perf.load_profile_ms,
            perf.read_events_ms,
            perf.aggregate_ms,
            perf.merge_ms,
            perf.save_profile_ms,
            perf.profile_store_ms,
            perf.total_ms,
            perf.run_entries
        );
    }
    if perf.load_profile_ms > 500 {
        eprintln!(
            "WARNING: profile load is slow ({}ms > 500ms trigger)",
            perf.load_profile_ms
        );
    }
    if perf.merge_ms > 1_000 {
        eprintln!(
            "WARNING: profile merge is slow ({}ms > 1000ms trigger)",
            perf.merge_ms
        );
    }
    if run_id_digest_evicted {
        eprintln!(
            "WARNING: run-id digest window is full ({} entries); old run-id dedupe evidence will be evicted over time",
            perf.run_id_digest_window_len
        );
    }

    if let Ok(path) = std::env::var("ASSAY_PROFILE_PERF_JSON") {
        let json = serde_json::to_string_pretty(&perf)?;
        std::fs::write(&path, json)
            .with_context(|| format!("failed to write profile perf json: {}", path))?;
        eprintln!("Wrote profile perf metrics: {}", path);
    }

    Ok(0)
}

fn cmd_show(args: ShowArgs) -> Result<i32> {
    let profile = load_profile(&args.profile)?;

    match args.format.as_str() {
        "json" => println!("{}", serde_json::to_string_pretty(&profile)?),
        "yaml" => println!("{}", serde_yaml::to_string(&profile)?),
        _ => show_summary(&profile, args.top),
    }

    Ok(0)
}

// ─────────────────────────────────────────────────────────────────────────────
// Aggregation & Merge
// ─────────────────────────────────────────────────────────────────────────────

#[derive(Debug, Default)]
struct RunData {
    files: BTreeMap<String, RunEntry>,
    network: BTreeMap<String, RunEntry>,
    processes: BTreeMap<String, RunEntry>,
}

#[derive(Debug, Default)]
struct RunEntry {
    timestamp: u64,
    hits: u64,
}

fn aggregate_run(events: &[Event]) -> RunData {
    let mut data = RunData::default();

    for ev in events {
        match ev {
            Event::FileOpen { path, timestamp } => {
                let e = data.files.entry(path.clone()).or_default();
                e.hits += 1;
                if *timestamp > e.timestamp {
                    e.timestamp = *timestamp;
                }
            }
            Event::NetConnect { dest, timestamp } => {
                let e = data.network.entry(dest.clone()).or_default();
                e.hits += 1;
                if *timestamp > e.timestamp {
                    e.timestamp = *timestamp;
                }
            }
            Event::ProcExec { path, timestamp } => {
                let e = data.processes.entry(path.clone()).or_default();
                e.hits += 1;
                if *timestamp > e.timestamp {
                    e.timestamp = *timestamp;
                }
            }
        }
    }

    data
}

fn merge_run(profile: &mut Profile, run: &RunData) -> (usize, usize) {
    let mut new_count = 0;
    let mut updated_count = 0;

    // Merge files
    for (key, run_entry) in &run.files {
        if let Some(entry) = profile.entries.files.get_mut(key) {
            entry.merge_run(run_entry.timestamp, run_entry.hits);
            updated_count += 1;
        } else {
            profile.entries.files.insert(
                key.clone(),
                ProfileEntry::new(run_entry.timestamp, run_entry.hits),
            );
            new_count += 1;
        }
    }

    // Merge network
    for (key, run_entry) in &run.network {
        if let Some(entry) = profile.entries.network.get_mut(key) {
            entry.merge_run(run_entry.timestamp, run_entry.hits);
            updated_count += 1;
        } else {
            profile.entries.network.insert(
                key.clone(),
                ProfileEntry::new(run_entry.timestamp, run_entry.hits),
            );
            new_count += 1;
        }
    }

    // Merge processes
    for (key, run_entry) in &run.processes {
        if let Some(entry) = profile.entries.processes.get_mut(key) {
            entry.merge_run(run_entry.timestamp, run_entry.hits);
            updated_count += 1;
        } else {
            profile.entries.processes.insert(
                key.clone(),
                ProfileEntry::new(run_entry.timestamp, run_entry.hits),
            );
            new_count += 1;
        }
    }

    (new_count, updated_count)
}

// ─────────────────────────────────────────────────────────────────────────────
// Summary Display
// ─────────────────────────────────────────────────────────────────────────────

fn show_summary(profile: &Profile, top_n: usize) {
    println!("Profile: {}", profile.name);
    println!("Version: {}", profile.version);
    if let Some(scope) = &profile.scope {
        println!("Scope: {}", scope);
    }
    println!("Created: {}", profile.created_at);
    println!("Updated: {}", profile.updated_at);
    println!("Total runs: {}", profile.total_runs);
    println!();
    println!("Entries:");
    println!("  Files: {}", profile.entries.files.len());
    println!("  Network: {}", profile.entries.network.len());
    println!("  Processes: {}", profile.entries.processes.len());
    println!();

    if profile.total_runs > 0 {
        println!("Stability distribution (α=1.0):");
        show_stability_distribution(&profile.entries.files, profile.total_runs, "  Files");
        show_stability_distribution(&profile.entries.network, profile.total_runs, "  Network");
        show_stability_distribution(
            &profile.entries.processes,
            profile.total_runs,
            "  Processes",
        );
        println!();

        println!("Top {} most stable files:", top_n);
        show_top_stable(&profile.entries.files, profile.total_runs, top_n);

        if !profile.entries.network.is_empty() {
            println!("\nTop {} most stable network destinations:", top_n);
            show_top_stable(&profile.entries.network, profile.total_runs, top_n);
        }
    }
}

fn show_stability_distribution(
    entries: &BTreeMap<String, ProfileEntry>,
    total_runs: u32,
    label: &str,
) {
    if entries.is_empty() {
        return;
    }

    let mut high = 0; // >= 0.8
    let mut mid = 0; // 0.6-0.8
    let mut low = 0; // < 0.6

    for entry in entries.values() {
        let s = stability_smoothed(entry.runs_seen, total_runs, DEFAULT_ALPHA);
        if s >= 0.8 {
            high += 1;
        } else if s >= 0.6 {
            mid += 1;
        } else {
            low += 1;
        }
    }

    println!(
        "{}: {} stable (≥0.8), {} medium (0.6-0.8), {} low (<0.6)",
        label, high, mid, low
    );
}

fn show_top_stable(entries: &BTreeMap<String, ProfileEntry>, total_runs: u32, n: usize) {
    let mut sorted: Vec<_> = entries
        .iter()
        .map(|(k, v)| {
            (
                k,
                v,
                stability_smoothed(v.runs_seen, total_runs, DEFAULT_ALPHA),
            )
        })
        .collect();

    sorted.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap());

    for (key, entry, stab) in sorted.into_iter().take(n) {
        let key_short = if key.len() > 50 { &key[..50] } else { key };
        println!(
            "  {:.2} ({:>2}/{:>2}) {}",
            stab, entry.runs_seen, total_runs, key_short
        );
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Tests
// ─────────────────────────────────────────────────────────────────────────────

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

    #[test]
    fn aggregate_dedup() {
        let events = vec![
            Event::FileOpen {
                path: "/a".into(),
                timestamp: 100,
            },
            Event::FileOpen {
                path: "/a".into(),
                timestamp: 200,
            },
            Event::FileOpen {
                path: "/b".into(),
                timestamp: 150,
            },
        ];
        let run = aggregate_run(&events);
        assert_eq!(run.files.len(), 2);
        assert_eq!(run.files["/a"].hits, 2);
        assert_eq!(run.files["/a"].timestamp, 200);
    }

    #[test]
    fn merge_new_entries() {
        let mut profile = Profile::new("test", None);
        let events = vec![Event::FileOpen {
            path: "/a".into(),
            timestamp: 100,
        }];
        let run = aggregate_run(&events);
        let (new, updated) = merge_run(&mut profile, &run);

        assert_eq!(new, 1);
        assert_eq!(updated, 0);
        assert_eq!(profile.entries.files["/a"].runs_seen, 1);
    }

    #[test]
    fn merge_existing_entries() {
        let mut profile = Profile::new("test", None);
        profile
            .entries
            .files
            .insert("/a".into(), ProfileEntry::new(100, 5));

        let events = vec![
            Event::FileOpen {
                path: "/a".into(),
                timestamp: 200,
            },
            Event::FileOpen {
                path: "/a".into(),
                timestamp: 200,
            },
        ];
        let run = aggregate_run(&events);
        let (new, updated) = merge_run(&mut profile, &run);

        assert_eq!(new, 0);
        assert_eq!(updated, 1);
        assert_eq!(profile.entries.files["/a"].runs_seen, 2);
        assert_eq!(profile.entries.files["/a"].hits_total, 7); // 5 + 2
    }

    #[test]
    fn scope_guard_mismatch() {
        let mut p = Profile::new("test", Some("scope-A".into()));
        let new_scope = Some("scope-B".to_string());

        // Mismatch without force -> Error
        let res = enforce_scope(&mut p, new_scope.as_ref(), false);
        assert!(res.is_err());
        assert!(res.unwrap_err().to_string().contains("Scope mismatch"));

        // Mismatch with force -> Ok (no change to profile scope effectively, runs just get merged)
        // Wait, current logic allows update but doesn't change profile scope. That's desired behavior.
        let res_force = enforce_scope(&mut p, new_scope.as_ref(), true);
        assert!(res_force.is_ok());
        assert_eq!(p.scope.as_deref(), Some("scope-A"));
    }

    #[test]
    fn scope_guard_init() {
        let mut p = Profile::new("test", None);
        let new_scope = Some("scope-init".to_string());

        // First time -> set scope
        assert!(enforce_scope(&mut p, new_scope.as_ref(), false).is_ok());
        assert_eq!(p.scope.as_deref(), Some("scope-init"));
    }

    #[test]
    fn scope_guard_noop() {
        let mut p = Profile::new("test", Some("scope-A".into()));

        // Matching scope -> Ok
        assert!(enforce_scope(&mut p, Some(&"scope-A".to_string()), false).is_ok());

        // No incoming scope -> Ok
        assert!(enforce_scope(&mut p, None, false).is_ok());
    }
}
