use std::fmt;
use std::io::Read;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time::{Duration, Instant};

use soma_studio_core::{
    AppConfig, WorkspaceTaskId, WorkspaceTaskRunRequest, WorkspaceTaskRunResponse,
};

use crate::workspace::{WorkspaceError, WorkspaceErrorKind};

const WORKSPACE_TASK_SHORT_TIMEOUT: Duration = Duration::from_secs(5);
const WORKSPACE_TASK_LONG_TIMEOUT: Duration = Duration::from_secs(120);
const WORKSPACE_TASK_MAX_OUTPUT_BYTES: usize = 64 * 1024;

pub type WorkspaceTaskResult<T> = Result<T, WorkspaceTaskError>;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorkspaceTaskErrorKind {
    InvalidRequest,
    PathNotFound,
    RunNotFound,
    Busy,
    Upstream,
    Internal,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceTaskError {
    kind: WorkspaceTaskErrorKind,
    message: String,
}

impl WorkspaceTaskError {
    pub fn invalid_request(message: impl Into<String>) -> Self {
        Self::new(WorkspaceTaskErrorKind::InvalidRequest, message)
    }

    pub fn path_not_found(message: impl Into<String>) -> Self {
        Self::new(WorkspaceTaskErrorKind::PathNotFound, message)
    }

    pub fn run_not_found(message: impl Into<String>) -> Self {
        Self::new(WorkspaceTaskErrorKind::RunNotFound, message)
    }

    pub fn busy(message: impl Into<String>) -> Self {
        Self::new(WorkspaceTaskErrorKind::Busy, message)
    }

    pub fn upstream(message: impl Into<String>) -> Self {
        Self::new(WorkspaceTaskErrorKind::Upstream, message)
    }

    pub fn internal(message: impl Into<String>) -> Self {
        Self::new(WorkspaceTaskErrorKind::Internal, message)
    }

    pub fn kind(&self) -> WorkspaceTaskErrorKind {
        self.kind
    }

    pub fn message(&self) -> &str {
        &self.message
    }

    pub fn api_code(&self) -> &'static str {
        match self.kind() {
            WorkspaceTaskErrorKind::InvalidRequest => "invalid_request",
            WorkspaceTaskErrorKind::PathNotFound => "workspace_task_path_not_found",
            WorkspaceTaskErrorKind::RunNotFound => "workspace_task_run_not_found",
            WorkspaceTaskErrorKind::Busy => "workspace_task_busy",
            WorkspaceTaskErrorKind::Upstream => "upstream_error",
            WorkspaceTaskErrorKind::Internal => "internal_error",
        }
    }

    pub fn status_code(&self) -> u16 {
        match self.kind() {
            WorkspaceTaskErrorKind::InvalidRequest => 400,
            WorkspaceTaskErrorKind::PathNotFound | WorkspaceTaskErrorKind::RunNotFound => 404,
            WorkspaceTaskErrorKind::Busy => 409,
            WorkspaceTaskErrorKind::Upstream => 502,
            WorkspaceTaskErrorKind::Internal => 500,
        }
    }

    fn new(kind: WorkspaceTaskErrorKind, message: impl Into<String>) -> Self {
        Self {
            kind,
            message: message.into(),
        }
    }
}

impl fmt::Display for WorkspaceTaskError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(&self.message)
    }
}

impl std::error::Error for WorkspaceTaskError {}

impl From<WorkspaceError> for WorkspaceTaskError {
    fn from(error: WorkspaceError) -> Self {
        match error.kind() {
            WorkspaceErrorKind::InvalidRequest
            | WorkspaceErrorKind::Conflict
            | WorkspaceErrorKind::FileStale
            | WorkspaceErrorKind::PreviewMissing => Self::invalid_request(error.to_string()),
            WorkspaceErrorKind::NotFound => Self::path_not_found(error.to_string()),
            WorkspaceErrorKind::Upstream => Self::upstream(error.to_string()),
            WorkspaceErrorKind::Internal => Self::internal(error.to_string()),
        }
    }
}

pub fn workspace_task_command_label(task_id: WorkspaceTaskId) -> &'static str {
    match task_id {
        WorkspaceTaskId::GitStatus => "git status --short --branch",
        WorkspaceTaskId::GitDiff => "git diff --no-ext-diff",
        WorkspaceTaskId::CargoCheck => "cargo check --workspace",
        WorkspaceTaskId::NpmCheck => "npm --prefix web run check",
    }
}

pub fn workspace_task_max_output_bytes() -> usize {
    WORKSPACE_TASK_MAX_OUTPUT_BYTES
}

pub fn workspace_task_response_path(
    config: &AppConfig,
    input: &WorkspaceTaskRunRequest,
) -> WorkspaceTaskResult<String> {
    let relative_path = workspace_task_relative_path(config, input.task_id, input.path.as_deref())?;
    Ok(response_path(&relative_path))
}

pub fn run_workspace_task(
    config: &AppConfig,
    input: WorkspaceTaskRunRequest,
) -> WorkspaceTaskResult<WorkspaceTaskRunResponse> {
    if !workspace_task_supports_inline(input.task_id) {
        return Err(WorkspaceTaskError::invalid_request(format!(
            "{} must use /api/workspace/task-runs",
            workspace_task_id_label(input.task_id)
        )));
    }
    run_workspace_task_with_cancel(config, input, Arc::new(AtomicBool::new(false)))
}

pub fn run_workspace_task_with_cancel(
    config: &AppConfig,
    input: WorkspaceTaskRunRequest,
    cancel_requested: Arc<AtomicBool>,
) -> WorkspaceTaskResult<WorkspaceTaskRunResponse> {
    match input.task_id {
        WorkspaceTaskId::GitStatus => {
            run_git_status(config, input.path.as_deref(), cancel_requested)
        }
        WorkspaceTaskId::GitDiff => run_git_diff(config, input.path.as_deref(), cancel_requested),
        WorkspaceTaskId::CargoCheck => {
            run_cargo_check(config, input.path.as_deref(), cancel_requested)
        }
        WorkspaceTaskId::NpmCheck => run_npm_check(config, input.path.as_deref(), cancel_requested),
    }
}

fn workspace_task_supports_inline(task_id: WorkspaceTaskId) -> bool {
    matches!(
        task_id,
        WorkspaceTaskId::GitStatus | WorkspaceTaskId::GitDiff
    )
}

fn run_git_status(
    config: &AppConfig,
    raw_path: Option<&str>,
    cancel_requested: Arc<AtomicBool>,
) -> WorkspaceTaskResult<WorkspaceTaskRunResponse> {
    let canonical_root = canonical_workspace_root(config)?;
    let relative_path = workspace_task_relative_path(config, WorkspaceTaskId::GitStatus, raw_path)?;

    let mut command = Command::new("git");
    command
        .current_dir(&canonical_root)
        .arg("status")
        .arg("--short")
        .arg("--branch");
    if !relative_path.is_empty() {
        command.arg("--").arg(&relative_path);
    }

    run_workspace_command(
        command,
        WorkspaceTaskId::GitStatus,
        relative_path,
        cancel_requested,
    )
}

struct CommandOutput {
    exit_code: Option<i32>,
    stdout: CapturedOutput,
    stderr: CapturedOutput,
    timed_out: bool,
    cancelled: bool,
    duration: Duration,
}

struct CapturedOutput {
    text: String,
    truncated: bool,
}

fn run_git_diff(
    config: &AppConfig,
    raw_path: Option<&str>,
    cancel_requested: Arc<AtomicBool>,
) -> WorkspaceTaskResult<WorkspaceTaskRunResponse> {
    let canonical_root = canonical_workspace_root(config)?;
    let relative_path = workspace_task_relative_path(config, WorkspaceTaskId::GitDiff, raw_path)?;

    let mut command = Command::new("git");
    command
        .current_dir(&canonical_root)
        .arg("diff")
        .arg("--no-ext-diff");
    if !relative_path.is_empty() {
        command.arg("--").arg(&relative_path);
    }

    run_workspace_command(
        command,
        WorkspaceTaskId::GitDiff,
        relative_path,
        cancel_requested,
    )
}

fn workspace_task_relative_path(
    config: &AppConfig,
    task_id: WorkspaceTaskId,
    raw_path: Option<&str>,
) -> WorkspaceTaskResult<String> {
    match task_id {
        WorkspaceTaskId::GitStatus | WorkspaceTaskId::GitDiff => {
            workspace_task_existing_relative_path(config, raw_path)
        }
        WorkspaceTaskId::CargoCheck | WorkspaceTaskId::NpmCheck => {
            workspace_task_root_only_relative_path(config, task_id, raw_path)
        }
    }
}

fn workspace_task_existing_relative_path(
    config: &AppConfig,
    raw_path: Option<&str>,
) -> WorkspaceTaskResult<String> {
    match raw_path.map(str::trim).filter(|path| !path.is_empty()) {
        Some(path) => {
            let (_target, relative_path) =
                crate::workspace::resolve_workspace_existing_path(config, path)?;
            Ok(relative_path)
        }
        None => Ok(String::new()),
    }
}

fn workspace_task_root_only_relative_path(
    config: &AppConfig,
    task_id: WorkspaceTaskId,
    raw_path: Option<&str>,
) -> WorkspaceTaskResult<String> {
    canonical_workspace_root(config)?;
    match raw_path
        .map(str::trim)
        .filter(|path| !path.is_empty() && *path != ".")
    {
        Some(_) => Err(WorkspaceTaskError::invalid_request(format!(
            "{} must run at the workspace root, without path-scoped execution",
            workspace_task_id_label(task_id)
        ))),
        None => Ok(String::new()),
    }
}

fn run_cargo_check(
    config: &AppConfig,
    raw_path: Option<&str>,
    cancel_requested: Arc<AtomicBool>,
) -> WorkspaceTaskResult<WorkspaceTaskRunResponse> {
    let canonical_root = canonical_workspace_root(config)?;
    let relative_path =
        workspace_task_relative_path(config, WorkspaceTaskId::CargoCheck, raw_path)?;

    let mut command = Command::new("cargo");
    command
        .current_dir(&canonical_root)
        .arg("check")
        .arg("--workspace");

    run_workspace_command(
        command,
        WorkspaceTaskId::CargoCheck,
        relative_path,
        cancel_requested,
    )
}

fn run_npm_check(
    config: &AppConfig,
    raw_path: Option<&str>,
    cancel_requested: Arc<AtomicBool>,
) -> WorkspaceTaskResult<WorkspaceTaskRunResponse> {
    let canonical_root = canonical_workspace_root(config)?;
    let relative_path = workspace_task_relative_path(config, WorkspaceTaskId::NpmCheck, raw_path)?;
    if !canonical_root.join("web").join("package.json").is_file() {
        return Err(WorkspaceTaskError::invalid_request(
            "npm_check requires web/package.json under workspace root",
        ));
    }

    let mut command = npm_command();
    command
        .current_dir(&canonical_root)
        .arg("--prefix")
        .arg("web")
        .arg("run")
        .arg("check");

    run_workspace_command(
        command,
        WorkspaceTaskId::NpmCheck,
        relative_path,
        cancel_requested,
    )
}

fn run_workspace_command(
    mut command: Command,
    task_id: WorkspaceTaskId,
    relative_path: String,
    cancel_requested: Arc<AtomicBool>,
) -> WorkspaceTaskResult<WorkspaceTaskRunResponse> {
    let output = run_command_with_timeout(
        &mut command,
        workspace_task_timeout(task_id),
        cancel_requested,
    )?;
    Ok(WorkspaceTaskRunResponse {
        task_id,
        path: response_path(&relative_path),
        command_label: workspace_task_command_label(task_id).to_string(),
        exit_code: output.exit_code,
        stdout: output.stdout.text,
        stderr: output.stderr.text,
        stdout_truncated: output.stdout.truncated,
        stderr_truncated: output.stderr.truncated,
        timed_out: output.timed_out,
        cancelled: output.cancelled,
        duration_ms: output.duration.as_millis() as u64,
        max_output_bytes: WORKSPACE_TASK_MAX_OUTPUT_BYTES,
    })
}

fn workspace_task_timeout(task_id: WorkspaceTaskId) -> Duration {
    match task_id {
        WorkspaceTaskId::GitStatus | WorkspaceTaskId::GitDiff => WORKSPACE_TASK_SHORT_TIMEOUT,
        WorkspaceTaskId::CargoCheck | WorkspaceTaskId::NpmCheck => WORKSPACE_TASK_LONG_TIMEOUT,
    }
}

fn canonical_workspace_root(config: &AppConfig) -> WorkspaceTaskResult<PathBuf> {
    config.project_root.canonicalize().map_err(|error| {
        WorkspaceTaskError::internal(format!("failed to canonicalize workspace root: {error}"))
    })
}

fn workspace_task_id_label(task_id: WorkspaceTaskId) -> &'static str {
    match task_id {
        WorkspaceTaskId::GitStatus => "git_status",
        WorkspaceTaskId::GitDiff => "git_diff",
        WorkspaceTaskId::CargoCheck => "cargo_check",
        WorkspaceTaskId::NpmCheck => "npm_check",
    }
}

fn npm_command() -> Command {
    if cfg!(windows) {
        Command::new("npm.cmd")
    } else {
        Command::new("npm")
    }
}

fn response_path(relative_path: &str) -> String {
    if relative_path.is_empty() {
        ".".to_string()
    } else {
        relative_path.to_string()
    }
}

fn run_command_with_timeout(
    command: &mut Command,
    timeout: Duration,
    cancel_requested: Arc<AtomicBool>,
) -> WorkspaceTaskResult<CommandOutput> {
    let mut child = command
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .map_err(|error| {
            WorkspaceTaskError::upstream(format!("failed to start workspace task: {error}"))
        })?;

    let stdout = child
        .stdout
        .take()
        .ok_or_else(|| WorkspaceTaskError::internal("failed to capture workspace task stdout"))?;
    let stderr = child
        .stderr
        .take()
        .ok_or_else(|| WorkspaceTaskError::internal("failed to capture workspace task stderr"))?;
    let stdout_reader =
        thread::spawn(move || read_limited_output(stdout, WORKSPACE_TASK_MAX_OUTPUT_BYTES));
    let stderr_reader =
        thread::spawn(move || read_limited_output(stderr, WORKSPACE_TASK_MAX_OUTPUT_BYTES));

    let started_at = Instant::now();
    let mut timed_out = false;
    let mut cancelled = false;
    let status = loop {
        match child.try_wait().map_err(|error| {
            WorkspaceTaskError::upstream(format!("failed to poll workspace task: {error}"))
        })? {
            Some(status) => break status,
            None if cancel_requested.load(Ordering::SeqCst) => {
                cancelled = true;
                let _ = child.kill();
                break child.wait().map_err(|error| {
                    WorkspaceTaskError::upstream(format!(
                        "failed to cancel workspace task: {error}"
                    ))
                })?;
            }
            None if started_at.elapsed() >= timeout => {
                timed_out = true;
                let _ = child.kill();
                break child.wait().map_err(|error| {
                    WorkspaceTaskError::upstream(format!(
                        "failed to stop timed out workspace task: {error}"
                    ))
                })?;
            }
            None => thread::sleep(Duration::from_millis(25)),
        }
    };

    let stdout = stdout_reader
        .join()
        .map_err(|_| WorkspaceTaskError::internal("workspace task stdout reader failed"))?;
    let stderr = stderr_reader
        .join()
        .map_err(|_| WorkspaceTaskError::internal("workspace task stderr reader failed"))?;

    Ok(CommandOutput {
        exit_code: status.code(),
        stdout,
        stderr,
        timed_out,
        cancelled,
        duration: started_at.elapsed(),
    })
}

fn read_limited_output<R: Read>(mut reader: R, limit: usize) -> CapturedOutput {
    let mut stored = Vec::new();
    let mut truncated = false;
    let mut buffer = [0_u8; 8192];

    loop {
        let bytes_read = match reader.read(&mut buffer) {
            Ok(0) => break,
            Ok(bytes_read) => bytes_read,
            Err(_) => {
                truncated = true;
                break;
            }
        };

        let available = limit.saturating_sub(stored.len());
        if available > 0 {
            let take = available.min(bytes_read);
            stored.extend_from_slice(&buffer[..take]);
            if take < bytes_read {
                truncated = true;
            }
        } else {
            truncated = true;
        }
    }

    CapturedOutput {
        text: String::from_utf8_lossy(&stored).to_string(),
        truncated,
    }
}

#[cfg(test)]
mod tests {
    use soma_studio_core::{AppConfig, WorkspaceTaskId, WorkspaceTaskRunRequest};
    use uuid::Uuid;

    use super::{
        WORKSPACE_TASK_MAX_OUTPUT_BYTES, WorkspaceTaskErrorKind, read_limited_output,
        run_workspace_task, workspace_task_response_path,
    };

    #[test]
    fn output_capture_marks_truncated_content() {
        let content = vec![b'a'; WORKSPACE_TASK_MAX_OUTPUT_BYTES + 16];
        let captured = read_limited_output(content.as_slice(), WORKSPACE_TASK_MAX_OUTPUT_BYTES);

        assert_eq!(captured.text.len(), WORKSPACE_TASK_MAX_OUTPUT_BYTES);
        assert!(captured.truncated);
    }

    #[test]
    fn inline_long_task_returns_typed_invalid_request() {
        let temp_dir = std::env::temp_dir().join(format!("soma-task-inline-{}", Uuid::new_v4()));
        std::fs::create_dir_all(&temp_dir).expect("workspace dir");
        let config = test_config(temp_dir.clone());
        let request = WorkspaceTaskRunRequest {
            task_id: WorkspaceTaskId::CargoCheck,
            path: None,
        };

        let error = run_workspace_task(&config, request).expect_err("inline long task error");

        assert_eq!(error.kind(), WorkspaceTaskErrorKind::InvalidRequest);
        assert_eq!(error.api_code(), "invalid_request");
        assert_eq!(error.status_code(), 400);

        let _ = std::fs::remove_dir_all(temp_dir);
    }

    #[test]
    fn root_only_task_rejects_path_scope_with_typed_invalid_request() {
        let temp_dir = std::env::temp_dir().join(format!("soma-task-scope-{}", Uuid::new_v4()));
        std::fs::create_dir_all(temp_dir.join("docs")).expect("docs dir");
        let config = test_config(temp_dir.clone());
        let request = WorkspaceTaskRunRequest {
            task_id: WorkspaceTaskId::CargoCheck,
            path: Some("docs".to_string()),
        };

        let error = workspace_task_response_path(&config, &request).expect_err("scoped task error");

        assert_eq!(error.kind(), WorkspaceTaskErrorKind::InvalidRequest);
        assert_eq!(error.api_code(), "invalid_request");
        assert_eq!(error.status_code(), 400);

        let _ = std::fs::remove_dir_all(temp_dir);
    }

    fn test_config(project_root: std::path::PathBuf) -> AppConfig {
        AppConfig {
            app_name: "Soma Studio".to_string(),
            bind_addr: "127.0.0.1:0".to_string(),
            data_dir: project_root.join(".soma-studio-data"),
            derived_dir: project_root.join(".soma-studio-data").join("derived"),
            notebook_dir: project_root.join(".soma-studio-data").join("notebook"),
            user_assets_dir: project_root.join(".soma-studio-data").join("assets"),
            db_path: project_root.join(".soma-studio-data").join("test.db"),
            web_build_dir: project_root.join("web").join("build"),
            web_shell_file: project_root.join("web").join("build").join("spa.html"),
            project_root,
        }
    }
}
