use anyhow::{Context, Result, bail};
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use vtcode_core::cli::args::{ExecResumeArgs, ExecSubcommand};
use vtcode_core::cli::input_hardening::validate_agent_safe_text;
use vtcode_core::config::VTCodeConfig;
use vtcode_core::config::models::ModelId;
use vtcode_core::config::types::AgentConfig as CoreAgentConfig;
use vtcode_core::core::threads::{
    ArchivedSessionIntent, SessionQueryScope, ThreadBootstrap, build_thread_archive_metadata,
    list_recent_sessions_in_scope, prepare_archived_session,
};
use vtcode_core::review::{ReviewSpec, build_review_prompt};
use vtcode_core::utils::session_archive::{
    SessionArchive, SessionListing, find_session_by_identifier,
    generate_session_archive_identifier, history_persistence_enabled,
    reserve_session_archive_identifier,
};
use vtcode_core::utils::tty::TtyExt;
use vtcode_core::utils::validation::validate_non_empty;

use crate::agent::agents::{apply_persisted_resume_metadata, apply_runtime_overrides};
use crate::startup::require_full_auto_workspace_trust;

use super::super::exec::ExecCommandOptions;

enum StdinPromptBehavior {
    RequiredIfPiped,
    Forced,
    OptionalAppend,
}

#[derive(Debug, Clone)]
pub enum ExecCommandKind {
    Run {
        prompt_arg: Option<String>,
    },
    Resume {
        prompt_arg: Option<String>,
        session_id: Option<String>,
        last: bool,
        all: bool,
    },
    Review {
        spec: ReviewSpec,
    },
}

pub(super) struct ExecPreparedRun {
    pub config: CoreAgentConfig,
    pub vt_cfg: VTCodeConfig,
    pub model_id: ModelId,
    pub prompt: String,
    pub session_id: String,
    pub archive: Option<SessionArchive>,
    pub thread_bootstrap: ThreadBootstrap,
}

pub(crate) fn resolve_exec_command(
    command: Option<ExecSubcommand>,
    prompt: Option<String>,
) -> Result<ExecCommandKind> {
    match command {
        Some(ExecSubcommand::Resume(resume)) => resolve_resume_command(resume),
        None => Ok(ExecCommandKind::Run { prompt_arg: prompt }),
    }
}

fn resolve_resume_command(resume: ExecResumeArgs) -> Result<ExecCommandKind> {
    if resume.last {
        if resume.prompt.is_some() {
            bail!(
                "Exec resume --last accepts a single prompt argument. Quote multi-word prompts or pipe stdin."
            );
        }

        return Ok(ExecCommandKind::Resume {
            prompt_arg: resume.session_or_prompt,
            session_id: None,
            last: true,
            all: resume.all,
        });
    }

    Ok(ExecCommandKind::Resume {
        prompt_arg: resume.prompt,
        session_id: resume.session_or_prompt,
        last: false,
        all: resume.all,
    })
}

pub(super) async fn prepare_exec_run(
    config: &CoreAgentConfig,
    vt_cfg: &VTCodeConfig,
    options: &ExecCommandOptions,
) -> Result<ExecPreparedRun> {
    validate_resume_prompt_requirement(&options.command, io::stdin().is_tty_ext())?;

    let (prompt, resume_listing) = match &options.command {
        ExecCommandKind::Run { prompt_arg } => {
            (resolve_prompt(prompt_arg.clone(), config.quiet)?, None)
        }
        ExecCommandKind::Resume { prompt_arg, .. } => (
            resolve_prompt(prompt_arg.clone(), config.quiet)?,
            Some(resolve_resume_listing(options, config).await?),
        ),
        ExecCommandKind::Review { spec } => (build_review_prompt(spec), None),
    };

    let mut run_config = config.clone();
    let run_workspace = if let Some(listing) = &resume_listing {
        apply_persisted_resume_metadata(&mut run_config, Some(&listing.snapshot.metadata));
        resolve_resume_workspace(listing)?
    } else {
        config.workspace.clone()
    };
    run_config.workspace = run_workspace.clone();

    let mut run_vt_cfg = load_exec_vt_config(vt_cfg, &run_workspace, &config.workspace).await?;
    apply_runtime_overrides(Some(&mut run_vt_cfg), &run_config);

    require_full_auto_workspace_trust(&run_config.workspace, "exec runs", "exec").await?;

    let automation_cfg = &run_vt_cfg.automation.full_auto;
    if !automation_cfg.enabled {
        bail!(
            "Automation is disabled in configuration. Enable [automation.full_auto] to continue."
        );
    }

    let model_id = ModelId::from_str(&run_config.model).with_context(|| {
        format!(
            "Model '{}' is not recognized for exec command. Update vtcode.toml to a supported identifier.",
            run_config.model
        )
    })?;

    let metadata =
        build_exec_archive_metadata(run_workspace.as_path(), &model_id, &run_vt_cfg, &run_config);
    let history_enabled = history_persistence_enabled();
    let reserved_archive_id = crate::main_helpers::runtime_archive_session_id();

    let (session_id, archive, thread_bootstrap) = if let Some(listing) = resume_listing {
        if history_enabled {
            let prepared = prepare_archived_session(
                listing,
                run_workspace.clone(),
                metadata.clone(),
                ArchivedSessionIntent::ResumeInPlace,
                None,
            )
            .await
            .context("Failed to prepare archived exec session")?;
            (
                prepared.thread_id,
                Some(prepared.archive),
                prepared.bootstrap,
            )
        } else {
            let session_id = listing.identifier();
            let mut bootstrap = ThreadBootstrap::from_listing(listing);
            bootstrap.metadata = Some(metadata.clone());
            (session_id, None, bootstrap)
        }
    } else {
        let session_id = next_exec_session_id(
            run_workspace.as_path(),
            reserved_archive_id,
            history_enabled,
        )
        .await?;
        let archive = if history_enabled {
            Some(
                SessionArchive::new_with_identifier(metadata.clone(), session_id.clone())
                    .await
                    .context("Failed to create exec session archive")?,
            )
        } else {
            None
        };
        let bootstrap = ThreadBootstrap::new(Some(metadata));
        (session_id, archive, bootstrap)
    };

    Ok(ExecPreparedRun {
        config: run_config,
        vt_cfg: run_vt_cfg,
        model_id,
        prompt,
        session_id,
        archive,
        thread_bootstrap,
    })
}

pub(super) fn validate_resume_prompt_requirement(
    command: &ExecCommandKind,
    stdin_is_tty: bool,
) -> Result<()> {
    if let ExecCommandKind::Resume { prompt_arg, .. } = command
        && prompt_arg.is_none()
        && stdin_is_tty
    {
        bail!(
            "Exec resume requires a follow-up prompt. Pass one explicitly, use '-' to read stdin, or pipe input."
        );
    }

    Ok(())
}

fn resolve_prompt(prompt_arg: Option<String>, quiet: bool) -> Result<String> {
    let prompt = match prompt_arg {
        Some(prompt) if prompt != "-" => {
            if let Some(stdin_text) =
                read_prompt_from_stdin(StdinPromptBehavior::OptionalAppend, quiet)?
            {
                prompt_with_stdin_context(&prompt, &stdin_text)
            } else {
                prompt
            }
        }
        maybe_dash => {
            let behavior = if matches!(maybe_dash.as_deref(), Some("-")) {
                StdinPromptBehavior::Forced
            } else {
                StdinPromptBehavior::RequiredIfPiped
            };
            read_prompt_from_stdin(behavior, quiet)?
                .expect("required stdin prompt should produce content")
        }
    };

    validate_agent_safe_text("prompt", &prompt)?;
    Ok(prompt)
}

fn read_prompt_from_stdin(behavior: StdinPromptBehavior, quiet: bool) -> Result<Option<String>> {
    let stdin_is_tty = io::stdin().is_tty_ext();

    match behavior {
        StdinPromptBehavior::RequiredIfPiped if stdin_is_tty => {
            bail!(
                "No prompt provided. Pass a prompt argument, pipe input, or use '-' to read from stdin."
            );
        }
        StdinPromptBehavior::RequiredIfPiped => {
            if !quiet {
                eprintln!("Reading prompt from stdin...");
            }
        }
        StdinPromptBehavior::Forced => {}
        StdinPromptBehavior::OptionalAppend if stdin_is_tty => return Ok(None),
        StdinPromptBehavior::OptionalAppend => {
            if !quiet {
                eprintln!("Reading additional input from stdin...");
            }
        }
    }

    let mut buffer = String::with_capacity(1024);
    io::stdin()
        .read_to_string(&mut buffer)
        .context("Failed to read prompt from stdin")?;

    if buffer.trim().is_empty() {
        return match behavior {
            StdinPromptBehavior::OptionalAppend => Ok(None),
            StdinPromptBehavior::RequiredIfPiped | StdinPromptBehavior::Forced => {
                validate_non_empty(&buffer, "Prompt via stdin")?;
                Ok(None)
            }
        };
    }

    Ok(Some(buffer))
}

fn prompt_with_stdin_context(prompt: &str, stdin_text: &str) -> String {
    let mut combined = format!("{prompt}\n\n<stdin>\n{stdin_text}");
    if !stdin_text.ends_with('\n') {
        combined.push('\n');
    }
    combined.push_str("</stdin>");
    combined
}

fn exec_workspace_label(workspace: &Path) -> String {
    workspace
        .file_name()
        .and_then(|component| component.to_str())
        .map(|value| value.to_string())
        .unwrap_or_else(|| "workspace".to_string())
}

fn resolve_resume_workspace(listing: &SessionListing) -> Result<PathBuf> {
    let workspace = listing.snapshot.metadata.workspace_path.trim();
    if workspace.is_empty() {
        bail!(
            "Archived exec session '{}' is missing workspace metadata.",
            listing.identifier()
        );
    }

    let path = PathBuf::from(workspace);
    if !path.exists() {
        bail!(
            "Archived exec workspace '{}' no longer exists on disk.",
            path.display()
        );
    }

    Ok(path)
}

fn build_exec_archive_metadata(
    workspace: &Path,
    model_id: &ModelId,
    vt_cfg: &VTCodeConfig,
    config: &CoreAgentConfig,
) -> vtcode_core::utils::session_archive::SessionArchiveMetadata {
    build_thread_archive_metadata(
        workspace,
        model_id.as_str(),
        &vt_cfg.agent.provider,
        &vt_cfg.agent.theme,
        config.reasoning_effort.as_str(),
    )
    .with_debug_log_path(
        crate::main_helpers::runtime_debug_log_path()
            .map(|path| path.to_string_lossy().to_string()),
    )
}

async fn next_exec_session_id(
    workspace: &Path,
    reserved_archive_id: Option<String>,
    history_enabled: bool,
) -> Result<String> {
    if let Some(identifier) = reserved_archive_id {
        return Ok(identifier);
    }

    let workspace_label = exec_workspace_label(workspace);
    if history_enabled {
        reserve_session_archive_identifier(&workspace_label, None)
            .await
            .context("Failed to reserve exec session archive identifier")
    } else {
        Ok(generate_session_archive_identifier(&workspace_label, None))
    }
}

async fn load_exec_vt_config(
    base: &VTCodeConfig,
    workspace: &Path,
    original_workspace: &Path,
) -> Result<VTCodeConfig> {
    if workspace == original_workspace {
        return Ok(base.clone());
    }

    let manager = crate::main_helpers::load_workspace_config(workspace)?;
    Ok(manager.config().clone())
}

async fn resolve_resume_listing(
    options: &ExecCommandOptions,
    config: &CoreAgentConfig,
) -> Result<SessionListing> {
    let ExecCommandKind::Resume {
        session_id,
        last,
        all,
        ..
    } = &options.command
    else {
        bail!("Internal error: resume listing requested for non-resume exec command");
    };

    if *last {
        let scope = if *all {
            SessionQueryScope::All
        } else {
            SessionQueryScope::CurrentWorkspace(config.workspace.clone())
        };
        return list_recent_sessions_in_scope(1, &scope)
            .await?
            .into_iter()
            .next()
            .context("No archived exec sessions were found.");
    }

    let identifier = session_id
        .as_deref()
        .context("Session id is required when --last is not used.")?;
    find_session_by_identifier(identifier)
        .await?
        .with_context(|| {
            format!("No archived exec session with identifier '{identifier}' was found.")
        })
}

#[cfg(test)]
mod tests {
    use super::{
        ExecCommandKind, build_exec_archive_metadata, next_exec_session_id,
        prompt_with_stdin_context, resolve_exec_command, validate_resume_prompt_requirement,
    };
    use std::path::{Path, PathBuf};
    use std::str::FromStr;
    use vtcode_core::cli::args::{ExecResumeArgs, ExecSubcommand};
    use vtcode_core::config::loader::VTCodeConfig;
    use vtcode_core::config::models::ModelId;
    use vtcode_core::config::types::AgentConfig as CoreAgentConfig;
    use vtcode_core::review::{ReviewTarget, build_review_spec};
    use vtcode_core::utils::session_debug::{
        configure_runtime_debug_context, runtime_debug_log_path, set_runtime_debug_log_path,
    };

    #[test]
    fn resolve_resume_last_uses_first_positional_as_prompt() {
        let command = resolve_exec_command(
            Some(ExecSubcommand::Resume(ExecResumeArgs {
                last: true,
                all: false,
                session_or_prompt: Some("continue".to_string()),
                prompt: None,
            })),
            None,
        )
        .expect("resume command should resolve");

        assert!(matches!(
            command,
            ExecCommandKind::Resume {
                last: true,
                session_id: None,
                prompt_arg: Some(ref prompt),
                all: _,
            } if prompt == "continue"
        ));
    }

    #[test]
    fn resolve_resume_last_rejects_second_positional() {
        let err = resolve_exec_command(
            Some(ExecSubcommand::Resume(ExecResumeArgs {
                last: true,
                all: false,
                session_or_prompt: Some("continue".to_string()),
                prompt: Some("extra".to_string()),
            })),
            None,
        )
        .expect_err("second positional should be rejected");

        assert!(err.to_string().contains("accepts a single prompt argument"));
    }

    #[test]
    fn resume_without_prompt_is_rejected_on_tty() {
        let err = validate_resume_prompt_requirement(
            &ExecCommandKind::Resume {
                prompt_arg: None,
                session_id: Some("session-1".to_string()),
                last: false,
                all: false,
            },
            true,
        )
        .expect_err("resume without prompt should fail");

        assert!(
            err.to_string()
                .contains("Exec resume requires a follow-up prompt")
        );
    }

    #[test]
    fn review_command_does_not_require_resume_prompt() {
        let spec = build_review_spec(false, None, Vec::new(), None).expect("review spec");
        let result = validate_resume_prompt_requirement(
            &ExecCommandKind::Review { spec: spec.clone() },
            true,
        );

        result.unwrap();
        assert!(matches!(spec.target, ReviewTarget::CurrentDiff));
    }

    #[test]
    fn prompt_with_stdin_context_wraps_stdin_block() {
        let combined = prompt_with_stdin_context("Summarize this concisely", "my output");

        assert_eq!(
            combined,
            "Summarize this concisely\n\n<stdin>\nmy output\n</stdin>"
        );
    }

    #[test]
    fn prompt_with_stdin_context_preserves_trailing_newline() {
        let combined = prompt_with_stdin_context("Summarize this concisely", "my output\n");

        assert_eq!(
            combined,
            "Summarize this concisely\n\n<stdin>\nmy output\n</stdin>"
        );
    }

    #[tokio::test]
    async fn next_exec_session_id_prefers_reserved_identifier() {
        let session_id =
            next_exec_session_id(Path::new("."), Some("session-reserved".to_string()), true)
                .await
                .expect("reserved session id should win");

        assert_eq!(session_id, "session-reserved");
    }

    #[test]
    fn build_exec_archive_metadata_includes_runtime_debug_log_path() {
        let mut vt_cfg = VTCodeConfig::default();
        vt_cfg.agent.provider = "openai".to_string();
        vt_cfg.agent.theme = "mono".to_string();

        let config = CoreAgentConfig {
            model: "gpt-5.4".to_string(),
            api_key: "test-key".to_string(),
            provider: "openai".to_string(),
            api_key_env: "OPENAI_API_KEY".to_string(),
            workspace: PathBuf::from("."),
            verbose: false,
            quiet: false,
            theme: "mono".to_string(),
            reasoning_effort: Default::default(),
            ui_surface: Default::default(),
            prompt_cache: Default::default(),
            model_source: Default::default(),
            custom_api_keys: Default::default(),
            checkpointing_enabled: true,
            checkpointing_storage_dir: None,
            checkpointing_max_snapshots: 50,
            checkpointing_max_age_days: Some(30),
            max_conversation_turns: 1000,
            model_behavior: None,
            openai_chatgpt_auth: None,
        };

        configure_runtime_debug_context("debug-session".to_string(), Some("session-1".to_string()));
        let path = PathBuf::from("/tmp/debug-session.log");
        set_runtime_debug_log_path(&path);
        let model_id = ModelId::from_str("gpt-5.4").expect("model id");
        let metadata = build_exec_archive_metadata(Path::new("."), &model_id, &vt_cfg, &config);

        assert_eq!(runtime_debug_log_path().as_deref(), Some(path.as_path()));
        assert_eq!(
            metadata.debug_log_path.as_deref(),
            Some("/tmp/debug-session.log")
        );
    }
}
