use std::cmp::Ordering;
use std::fmt;
use std::path::{Component, Path, PathBuf};
use std::time::UNIX_EPOCH;

use soma_studio_core::{
    AppConfig, WorkspaceEntryKind, WorkspaceFileChangeAction, WorkspaceFileChangeApplyResponse,
    WorkspaceFileChangePreviewRequest, WorkspaceFileChangePreviewResponse, WorkspaceFileEntry,
    WorkspaceFileListResponse, WorkspaceFilePreviewResponse,
};

const WORKSPACE_PREVIEW_MAX_BYTES: u64 = 256 * 1024;
const WORKSPACE_CHANGE_MAX_CONTENT_BYTES: usize = 512 * 1024;
const WORKSPACE_CHANGE_DIFF_PREVIEW_MAX_BYTES: usize = 64 * 1024;

pub type WorkspaceResult<T> = Result<T, WorkspaceError>;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorkspaceErrorKind {
    InvalidRequest,
    NotFound,
    Conflict,
    FileStale,
    PreviewMissing,
    Upstream,
    Internal,
}

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

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

    pub fn not_found(message: impl Into<String>) -> Self {
        Self::new(WorkspaceErrorKind::NotFound, message)
    }

    pub fn conflict(message: impl Into<String>) -> Self {
        Self::new(WorkspaceErrorKind::Conflict, message)
    }

    pub fn file_stale(message: impl Into<String>) -> Self {
        Self::new(WorkspaceErrorKind::FileStale, message)
    }

    pub fn preview_missing(message: impl Into<String>) -> Self {
        Self::new(WorkspaceErrorKind::PreviewMissing, message)
    }

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

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

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

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

    pub fn api_code(&self) -> &'static str {
        match self.kind() {
            WorkspaceErrorKind::InvalidRequest => "invalid_request",
            WorkspaceErrorKind::NotFound => "not_found",
            WorkspaceErrorKind::Conflict => "workspace_conflict",
            WorkspaceErrorKind::FileStale => "workspace_file_stale",
            WorkspaceErrorKind::PreviewMissing => "workspace_file_change_preview_missing",
            WorkspaceErrorKind::Upstream => "upstream_error",
            WorkspaceErrorKind::Internal => "internal_error",
        }
    }

    pub fn status_code(&self) -> u16 {
        match self.kind() {
            WorkspaceErrorKind::InvalidRequest => 400,
            WorkspaceErrorKind::NotFound | WorkspaceErrorKind::PreviewMissing => 404,
            WorkspaceErrorKind::Conflict | WorkspaceErrorKind::FileStale => 409,
            WorkspaceErrorKind::Upstream => 502,
            WorkspaceErrorKind::Internal => 500,
        }
    }

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

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

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

pub fn list_workspace_files(
    config: &AppConfig,
    raw_path: Option<&str>,
) -> WorkspaceResult<WorkspaceFileListResponse> {
    let canonical_root = config.project_root.canonicalize().map_err(|error| {
        WorkspaceError::internal(format!("failed to canonicalize workspace root: {error}"))
    })?;
    let segments = parse_workspace_path(raw_path.unwrap_or_default())?;
    let relative_path = segments.join("/");
    let target = resolve_workspace_path(&canonical_root, &segments)?;

    if !target.is_dir() {
        return Err(WorkspaceError::invalid_request(format!(
            "workspace path must be a directory: {}",
            relative_path_label(&relative_path)
        )));
    }

    let mut entries = Vec::new();
    for entry in std::fs::read_dir(&target).map_err(|error| {
        WorkspaceError::internal(format!("failed to read workspace directory: {error}"))
    })? {
        let entry = entry.map_err(|error| {
            WorkspaceError::internal(format!("failed to read workspace entry: {error}"))
        })?;
        let file_type = entry.file_type().map_err(|error| {
            WorkspaceError::internal(format!("failed to inspect workspace entry type: {error}"))
        })?;
        if file_type.is_symlink() {
            continue;
        }
        if !file_type.is_dir() && !file_type.is_file() {
            continue;
        }

        let name = entry
            .file_name()
            .to_str()
            .ok_or_else(|| {
                WorkspaceError::invalid_request("workspace entry name must be valid UTF-8")
            })?
            .to_string();
        let kind = if file_type.is_dir() {
            WorkspaceEntryKind::Directory
        } else {
            WorkspaceEntryKind::File
        };
        let metadata = entry.metadata().map_err(|error| {
            WorkspaceError::internal(format!(
                "failed to inspect workspace entry metadata: {error}"
            ))
        })?;
        entries.push(WorkspaceFileEntry {
            path: join_relative_path(&relative_path, &name),
            name,
            kind,
            size_bytes: (kind == WorkspaceEntryKind::File).then_some(metadata.len()),
            modified_at_ms: modified_at_ms(&metadata),
        });
    }

    entries.sort_by(compare_workspace_entries);

    Ok(WorkspaceFileListResponse {
        root: canonical_root.to_string_lossy().to_string(),
        path: relative_path.clone(),
        parent_path: parent_workspace_path(&segments),
        entries,
    })
}

pub fn preview_workspace_file(
    config: &AppConfig,
    raw_path: Option<&str>,
) -> WorkspaceResult<WorkspaceFilePreviewResponse> {
    let canonical_root = config.project_root.canonicalize().map_err(|error| {
        WorkspaceError::internal(format!("failed to canonicalize workspace root: {error}"))
    })?;
    let segments = parse_workspace_path(raw_path.unwrap_or_default())?;
    if segments.is_empty() {
        return Err(WorkspaceError::invalid_request(
            "workspace file path is required",
        ));
    }
    let relative_path = segments.join("/");
    let target = resolve_workspace_path(&canonical_root, &segments)?;

    if !target.is_file() {
        return Err(WorkspaceError::invalid_request(format!(
            "workspace preview path must be a file: {}",
            relative_path_label(&relative_path)
        )));
    }
    if !is_previewable_text_path(&target) {
        return Err(WorkspaceError::invalid_request(format!(
            "unsupported workspace preview file type: {}",
            relative_path_label(&relative_path)
        )));
    }

    let metadata = std::fs::metadata(&target).map_err(|error| {
        WorkspaceError::internal(format!("failed to inspect workspace preview file: {error}"))
    })?;
    if metadata.len() > WORKSPACE_PREVIEW_MAX_BYTES {
        return Err(WorkspaceError::invalid_request(format!(
            "workspace preview file must be {WORKSPACE_PREVIEW_MAX_BYTES} bytes or smaller"
        )));
    }
    let content = std::fs::read_to_string(&target).map_err(|error| {
        WorkspaceError::invalid_request(format!("invalid UTF-8 workspace preview file: {error}"))
    })?;

    Ok(WorkspaceFilePreviewResponse {
        root: canonical_root.to_string_lossy().to_string(),
        path: relative_path,
        size_bytes: metadata.len(),
        modified_at_ms: modified_at_ms(&metadata),
        content,
        truncated: false,
        max_bytes: WORKSPACE_PREVIEW_MAX_BYTES,
    })
}

pub fn preview_workspace_file_change(
    config: &AppConfig,
    input: &WorkspaceFileChangePreviewRequest,
    preview_token: uuid::Uuid,
    expires_at_ms: u64,
) -> WorkspaceResult<(WorkspaceFileChangePreviewResponse, Option<u64>, Option<u64>)> {
    let (target, relative_path) = resolve_workspace_change_target(config, &input.path)?;
    let before = read_workspace_change_before(&target, input.action)?;
    if let Some(expected) = input.expected_modified_at_ms
        && before.modified_at_ms != Some(expected)
    {
        return Err(WorkspaceError::file_stale(
            "workspace file revision must match before preview",
        ));
    }
    let rename_target = workspace_change_rename_target(config, input, &relative_path)?;
    let next_content = workspace_change_next_content(input, before.content.as_deref())?;
    let (diff_preview, diff_truncated) = match &rename_target {
        Some((_, target_relative_path)) => {
            rename_diff_preview(&relative_path, target_relative_path)
        }
        None => diff_preview(
            before.content.as_deref(),
            next_content.as_deref(),
            &relative_path,
        ),
    };
    let size_bytes_after = match input.action {
        WorkspaceFileChangeAction::DeleteFile => None,
        WorkspaceFileChangeAction::RenamePath => before.size_bytes,
        WorkspaceFileChangeAction::WriteText => {
            next_content.as_ref().map(|content| content.len() as u64)
        }
    };
    let response = WorkspaceFileChangePreviewResponse {
        preview_token,
        action: input.action,
        path: relative_path,
        target_path: rename_target.map(|(_, target_relative_path)| target_relative_path),
        exists_before: before.exists,
        size_bytes_before: before.size_bytes,
        modified_at_ms_before: before.modified_at_ms,
        size_bytes_after,
        diff_preview,
        diff_truncated,
        expires_at_ms,
        max_content_bytes: WORKSPACE_CHANGE_MAX_CONTENT_BYTES,
    };
    Ok((response, before.modified_at_ms, before.size_bytes))
}

pub fn apply_workspace_file_change(
    config: &AppConfig,
    input: &WorkspaceFileChangePreviewRequest,
    preview_modified_at_ms: Option<u64>,
    preview_size_bytes: Option<u64>,
) -> WorkspaceResult<WorkspaceFileChangeApplyResponse> {
    let (target, relative_path) = resolve_workspace_change_target(config, &input.path)?;
    let before = read_workspace_change_before(&target, input.action)?;
    if before.modified_at_ms != preview_modified_at_ms || before.size_bytes != preview_size_bytes {
        return Err(WorkspaceError::file_stale(
            "workspace file is stale; create a new preview",
        ));
    }
    let rename_target = workspace_change_rename_target(config, input, &relative_path)?;

    let mut after_target = target.clone();
    match input.action {
        WorkspaceFileChangeAction::WriteText => {
            let content = workspace_change_required_content(input)?;
            std::fs::write(&target, content).map_err(|error| {
                WorkspaceError::internal(format!("failed to write workspace file: {error}"))
            })?;
        }
        WorkspaceFileChangeAction::DeleteFile => {
            if !before.exists {
                return Err(WorkspaceError::invalid_request(
                    "workspace file delete target must exist",
                ));
            }
            std::fs::remove_file(&target).map_err(|error| {
                WorkspaceError::internal(format!("failed to delete workspace file: {error}"))
            })?;
        }
        WorkspaceFileChangeAction::RenamePath => {
            let Some((destination, _)) = &rename_target else {
                return Err(WorkspaceError::invalid_request(
                    "workspace rename target path is required",
                ));
            };
            rename_workspace_file_without_overwrite(&target, destination)?;
            after_target = destination.clone();
        }
    }

    let after_metadata = std::fs::metadata(&after_target).ok();
    Ok(WorkspaceFileChangeApplyResponse {
        applied: true,
        action: input.action,
        path: relative_path,
        target_path: rename_target.map(|(_, target_relative_path)| target_relative_path),
        exists_after: after_metadata.is_some(),
        size_bytes_after: after_metadata.as_ref().map(std::fs::Metadata::len),
        modified_at_ms_after: after_metadata.as_ref().and_then(modified_at_ms),
    })
}

pub fn resolve_workspace_directory(config: &AppConfig, raw_path: &str) -> WorkspaceResult<PathBuf> {
    let (target, relative_path) = resolve_workspace_existing_path(config, raw_path)?;
    if !target.is_dir() {
        return Err(WorkspaceError::invalid_request(format!(
            "workspace path must be a directory: {}",
            relative_path_label(&relative_path)
        )));
    }
    Ok(target)
}

struct WorkspaceChangeBefore {
    exists: bool,
    size_bytes: Option<u64>,
    modified_at_ms: Option<u64>,
    content: Option<String>,
}

fn resolve_workspace_change_target(
    config: &AppConfig,
    raw_path: &str,
) -> WorkspaceResult<(PathBuf, String)> {
    let canonical_root = config.project_root.canonicalize().map_err(|error| {
        WorkspaceError::internal(format!("failed to canonicalize workspace root: {error}"))
    })?;
    let segments = parse_workspace_path(raw_path)?;
    if segments.is_empty() {
        return Err(WorkspaceError::invalid_request(
            "workspace file path is required",
        ));
    }
    let relative_path = segments.join("/");
    let parent_segments = &segments[..segments.len() - 1];
    let parent = resolve_workspace_path(&canonical_root, parent_segments)?;
    if !parent.is_dir() {
        return Err(WorkspaceError::invalid_request(
            "workspace file parent must be a directory",
        ));
    }
    let target = parent.join(
        segments
            .last()
            .ok_or_else(|| WorkspaceError::invalid_request("workspace file path is required"))?,
    );
    if let Ok(metadata) = std::fs::symlink_metadata(&target) {
        if metadata.file_type().is_symlink() {
            return Err(WorkspaceError::invalid_request(
                "workspace file target is blocked because it is a symlink",
            ));
        }
        if metadata.is_dir() {
            return Err(WorkspaceError::invalid_request(
                "workspace file change target must be a file",
            ));
        }
    }
    if target.try_exists().map_err(|error| {
        WorkspaceError::internal(format!("failed to inspect workspace file target: {error}"))
    })? {
        let canonical_target = target.canonicalize().map_err(|error| {
            WorkspaceError::internal(format!(
                "failed to canonicalize workspace file target: {error}"
            ))
        })?;
        if !canonical_target.starts_with(&canonical_root) {
            return Err(WorkspaceError::invalid_request(
                "workspace file target escapes the project root",
            ));
        }
    }
    if !is_previewable_text_path(&target) {
        return Err(WorkspaceError::invalid_request(format!(
            "unsupported workspace file change type: {}",
            relative_path_label(&relative_path)
        )));
    }
    Ok((target, relative_path))
}

fn read_workspace_change_before(
    target: &Path,
    action: WorkspaceFileChangeAction,
) -> WorkspaceResult<WorkspaceChangeBefore> {
    if !target.try_exists().map_err(|error| {
        WorkspaceError::internal(format!("failed to inspect workspace file: {error}"))
    })? {
        if action != WorkspaceFileChangeAction::WriteText {
            return Err(WorkspaceError::invalid_request(
                "workspace file change target must exist",
            ));
        }
        return Ok(WorkspaceChangeBefore {
            exists: false,
            size_bytes: None,
            modified_at_ms: None,
            content: None,
        });
    }

    let metadata = std::fs::metadata(target).map_err(|error| {
        WorkspaceError::internal(format!("failed to inspect workspace file: {error}"))
    })?;
    if metadata.len() > WORKSPACE_PREVIEW_MAX_BYTES {
        return Err(WorkspaceError::invalid_request(format!(
            "workspace file change target must be {WORKSPACE_PREVIEW_MAX_BYTES} bytes or smaller"
        )));
    }
    let content = std::fs::read_to_string(target).map_err(|error| {
        WorkspaceError::invalid_request(format!("invalid UTF-8 workspace file: {error}"))
    })?;
    Ok(WorkspaceChangeBefore {
        exists: true,
        size_bytes: Some(metadata.len()),
        modified_at_ms: modified_at_ms(&metadata),
        content: Some(content),
    })
}

fn workspace_change_next_content(
    input: &WorkspaceFileChangePreviewRequest,
    current_content: Option<&str>,
) -> WorkspaceResult<Option<String>> {
    match input.action {
        WorkspaceFileChangeAction::WriteText => Ok(Some(workspace_change_required_content(input)?)),
        WorkspaceFileChangeAction::DeleteFile => {
            if input.content.is_some() {
                return Err(WorkspaceError::invalid_request(
                    "workspace delete preview must not include content",
                ));
            }
            if current_content.is_none() {
                return Err(WorkspaceError::invalid_request(
                    "workspace file delete target must exist",
                ));
            }
            Ok(None)
        }
        WorkspaceFileChangeAction::RenamePath => {
            if input.content.is_some() {
                return Err(WorkspaceError::invalid_request(
                    "workspace rename preview must not include content",
                ));
            }
            if current_content.is_none() {
                return Err(WorkspaceError::invalid_request(
                    "workspace file rename target must exist",
                ));
            }
            Ok(current_content.map(ToString::to_string))
        }
    }
}

fn workspace_change_rename_target(
    config: &AppConfig,
    input: &WorkspaceFileChangePreviewRequest,
    source_relative_path: &str,
) -> WorkspaceResult<Option<(PathBuf, String)>> {
    if input.action != WorkspaceFileChangeAction::RenamePath {
        if input.target_path.is_some() {
            return Err(WorkspaceError::invalid_request(
                "workspace target path is only valid for rename",
            ));
        }
        return Ok(None);
    }
    let target_path = input.target_path.as_deref().ok_or_else(|| {
        WorkspaceError::invalid_request("workspace rename target path is required")
    })?;
    let (target, target_relative_path) = resolve_workspace_change_target(config, target_path)?;
    if target_relative_path == source_relative_path {
        return Err(WorkspaceError::invalid_request(
            "workspace rename target path must differ from source path",
        ));
    }
    if target.try_exists().map_err(|error| {
        WorkspaceError::internal(format!(
            "failed to inspect workspace rename target: {error}"
        ))
    })? {
        return Err(WorkspaceError::conflict(
            "workspace rename target already exists",
        ));
    }
    Ok(Some((target, target_relative_path)))
}

fn workspace_change_required_content(
    input: &WorkspaceFileChangePreviewRequest,
) -> WorkspaceResult<String> {
    let content = input.content.as_ref().ok_or_else(|| {
        WorkspaceError::invalid_request("workspace write preview content is required")
    })?;
    if content.len() > WORKSPACE_CHANGE_MAX_CONTENT_BYTES {
        return Err(WorkspaceError::invalid_request(format!(
            "workspace write preview content must be {WORKSPACE_CHANGE_MAX_CONTENT_BYTES} bytes or smaller"
        )));
    }
    Ok(content.clone())
}

fn diff_preview(before: Option<&str>, after: Option<&str>, path: &str) -> (String, bool) {
    let mut output = String::new();
    output.push_str(&format!("--- {path}\n+++ {path}\n"));
    match (before, after) {
        (None, Some(after)) => push_prefixed_lines(&mut output, '+', after),
        (Some(before), None) => push_prefixed_lines(&mut output, '-', before),
        (Some(before), Some(after)) if before == after => output.push_str(" no content changes\n"),
        (Some(before), Some(after)) => {
            push_prefixed_lines(&mut output, '-', before);
            push_prefixed_lines(&mut output, '+', after);
        }
        (None, None) => output.push_str(" no content changes\n"),
    }
    truncate_string(output, WORKSPACE_CHANGE_DIFF_PREVIEW_MAX_BYTES)
}

fn rename_diff_preview(source_path: &str, target_path: &str) -> (String, bool) {
    truncate_string(
        format!("rename from {source_path}\nrename to {target_path}\n"),
        WORKSPACE_CHANGE_DIFF_PREVIEW_MAX_BYTES,
    )
}

fn rename_workspace_file_without_overwrite(
    source: &Path,
    destination: &Path,
) -> WorkspaceResult<()> {
    let bytes = std::fs::read(source).map_err(|error| {
        WorkspaceError::internal(format!("failed to read workspace rename source: {error}"))
    })?;
    let mut destination_file = std::fs::OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(destination)
        .map_err(|error| {
            if destination.try_exists().unwrap_or(false) {
                WorkspaceError::conflict("workspace rename target already exists")
            } else {
                WorkspaceError::internal(format!(
                    "failed to create workspace rename target without overwrite: {error}"
                ))
            }
        })?;
    if let Err(error) = std::io::Write::write_all(&mut destination_file, &bytes) {
        let _ = std::fs::remove_file(destination);
        return Err(WorkspaceError::internal(format!(
            "failed to write workspace rename target: {error}"
        )));
    }
    if let Err(error) = destination_file.sync_all() {
        let _ = std::fs::remove_file(destination);
        return Err(WorkspaceError::internal(format!(
            "failed to flush workspace rename target: {error}"
        )));
    }
    drop(destination_file);
    if let Err(error) = std::fs::remove_file(source) {
        let _ = std::fs::remove_file(destination);
        return Err(WorkspaceError::internal(format!(
            "failed to remove workspace rename source: {error}"
        )));
    }
    Ok(())
}

fn push_prefixed_lines(output: &mut String, prefix: char, content: &str) {
    if content.is_empty() {
        output.push(prefix);
        output.push('\n');
        return;
    }
    for line in content.lines() {
        output.push(prefix);
        output.push_str(line);
        output.push('\n');
    }
}

fn truncate_string(mut value: String, max_bytes: usize) -> (String, bool) {
    if value.len() <= max_bytes {
        return (value, false);
    }
    let mut boundary = max_bytes;
    while !value.is_char_boundary(boundary) {
        boundary -= 1;
    }
    value.truncate(boundary);
    value.push_str("\n... truncated ...\n");
    (value, true)
}

pub fn resolve_workspace_existing_path(
    config: &AppConfig,
    raw_path: &str,
) -> WorkspaceResult<(PathBuf, String)> {
    let canonical_root = config.project_root.canonicalize().map_err(|error| {
        WorkspaceError::internal(format!("failed to canonicalize workspace root: {error}"))
    })?;
    let segments = parse_workspace_path(raw_path)?;
    let relative_path = segments.join("/");
    let target = resolve_workspace_path(&canonical_root, &segments)?;
    Ok((target, relative_path))
}

fn parse_workspace_path(raw_path: &str) -> WorkspaceResult<Vec<String>> {
    let normalized = raw_path.trim().replace('\\', "/");
    if normalized.is_empty() || normalized == "." {
        return Ok(Vec::new());
    }

    let mut segments = Vec::new();
    for component in Path::new(&normalized).components() {
        match component {
            Component::Normal(segment) => {
                let segment = segment.to_str().ok_or_else(|| {
                    WorkspaceError::invalid_request("workspace path segment must be valid UTF-8")
                })?;
                if segment.is_empty() {
                    return Err(WorkspaceError::invalid_request(
                        "workspace path contains an empty segment",
                    ));
                }
                if segment.contains(':') {
                    return Err(WorkspaceError::invalid_request(
                        "workspace path must not contain drive or stream separators",
                    ));
                }
                segments.push(segment.to_string());
            }
            Component::CurDir => {}
            Component::ParentDir | Component::RootDir | Component::Prefix(_) => {
                return Err(WorkspaceError::invalid_request(
                    "workspace path must stay inside the project root",
                ));
            }
        }
    }
    Ok(segments)
}

fn resolve_workspace_path(root: &Path, segments: &[String]) -> WorkspaceResult<PathBuf> {
    let mut target = root.to_path_buf();
    for segment in segments {
        target.push(segment);
        if let Ok(metadata) = std::fs::symlink_metadata(&target)
            && metadata.file_type().is_symlink()
        {
            return Err(WorkspaceError::invalid_request(
                "workspace path segment is blocked because it is a symlink",
            ));
        }
    }
    if !target.try_exists().map_err(|error| {
        WorkspaceError::internal(format!("failed to inspect workspace path: {error}"))
    })? {
        let relative_path = segments.join("/");
        return Err(WorkspaceError::not_found(format!(
            "workspace path not found: {}",
            relative_path_label(&relative_path)
        )));
    }

    let canonical_target = target.canonicalize().map_err(|error| {
        WorkspaceError::internal(format!("failed to canonicalize workspace path: {error}"))
    })?;
    if !canonical_target.starts_with(root) {
        return Err(WorkspaceError::invalid_request(
            "workspace path escapes the project root",
        ));
    }
    Ok(canonical_target)
}

fn compare_workspace_entries(left: &WorkspaceFileEntry, right: &WorkspaceFileEntry) -> Ordering {
    match (left.kind, right.kind) {
        (WorkspaceEntryKind::Directory, WorkspaceEntryKind::File) => Ordering::Less,
        (WorkspaceEntryKind::File, WorkspaceEntryKind::Directory) => Ordering::Greater,
        _ => left.name.to_lowercase().cmp(&right.name.to_lowercase()),
    }
}

fn join_relative_path(parent: &str, name: &str) -> String {
    if parent.is_empty() {
        name.to_string()
    } else {
        format!("{parent}/{name}")
    }
}

fn parent_workspace_path(segments: &[String]) -> Option<String> {
    if segments.is_empty() {
        return None;
    }
    Some(segments[..segments.len() - 1].join("/"))
}

fn relative_path_label(path: &str) -> &str {
    if path.is_empty() { "." } else { path }
}

fn is_previewable_text_path(path: &Path) -> bool {
    let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
        return false;
    };
    let file_name = file_name.to_lowercase();
    if matches!(
        file_name.as_str(),
        ".gitignore" | ".gitattributes" | "license" | "notice"
    ) {
        return true;
    }

    let Some(extension) = path
        .extension()
        .and_then(|extension| extension.to_str())
        .map(|extension| extension.to_lowercase())
    else {
        return false;
    };

    matches!(
        extension.as_str(),
        "bat"
            | "c"
            | "cmd"
            | "conf"
            | "cpp"
            | "cs"
            | "css"
            | "cts"
            | "csv"
            | "go"
            | "gradle"
            | "h"
            | "hpp"
            | "html"
            | "ini"
            | "java"
            | "js"
            | "jsx"
            | "json"
            | "kt"
            | "lock"
            | "log"
            | "mjs"
            | "md"
            | "mts"
            | "properties"
            | "ps1"
            | "py"
            | "rs"
            | "sh"
            | "sql"
            | "svelte"
            | "toml"
            | "ts"
            | "tsx"
            | "txt"
            | "typ"
            | "vue"
            | "xml"
            | "yaml"
            | "yml"
    )
}

fn modified_at_ms(metadata: &std::fs::Metadata) -> Option<u64> {
    metadata
        .modified()
        .ok()
        .and_then(|modified| modified.duration_since(UNIX_EPOCH).ok())
        .map(|duration| duration.as_millis() as u64)
}

#[cfg(test)]
mod tests {
    use soma_studio_core::{
        AppConfig, WorkspaceEntryKind, WorkspaceFileChangeAction, WorkspaceFileChangePreviewRequest,
    };
    use uuid::Uuid;

    use super::{
        WORKSPACE_PREVIEW_MAX_BYTES, WorkspaceErrorKind, list_workspace_files,
        preview_workspace_file, preview_workspace_file_change,
    };

    #[test]
    fn lists_directories_before_files_inside_project_root() {
        let temp_dir =
            std::env::temp_dir().join(format!("soma-workspace-files-{}", Uuid::new_v4()));
        std::fs::create_dir_all(temp_dir.join("docs")).expect("docs dir");
        std::fs::write(temp_dir.join("README.md"), "# Readme").expect("readme");
        let config = test_config(temp_dir.clone());

        let response = list_workspace_files(&config, None).expect("workspace files");

        assert_eq!(response.path, "");
        assert_eq!(response.parent_path, None);
        assert_eq!(response.entries[0].name, "docs");
        assert_eq!(response.entries[0].kind, WorkspaceEntryKind::Directory);
        assert_eq!(response.entries[1].name, "README.md");
        assert_eq!(response.entries[1].kind, WorkspaceEntryKind::File);

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

    #[test]
    fn rejects_paths_outside_project_root() {
        let temp_dir =
            std::env::temp_dir().join(format!("soma-workspace-files-{}", Uuid::new_v4()));
        std::fs::create_dir_all(&temp_dir).expect("temp dir");
        let config = test_config(temp_dir.clone());

        assert!(list_workspace_files(&config, Some("..")).is_err());
        assert!(list_workspace_files(&config, Some("/tmp")).is_err());
        assert!(list_workspace_files(&config, Some("C:/tmp")).is_err());
        assert!(list_workspace_files(&config, Some("missing")).is_err());

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

    #[test]
    fn previews_small_utf8_text_files_only() {
        let temp_dir =
            std::env::temp_dir().join(format!("soma-workspace-preview-{}", Uuid::new_v4()));
        std::fs::create_dir_all(temp_dir.join("docs")).expect("docs dir");
        std::fs::write(temp_dir.join("docs").join("guide.md"), "# Guide").expect("guide");
        std::fs::write(temp_dir.join("image.bin"), [0, 159, 146, 150]).expect("binary");
        let oversized = temp_dir.join("huge.txt");
        std::fs::write(
            &oversized,
            "a".repeat((WORKSPACE_PREVIEW_MAX_BYTES + 1) as usize),
        )
        .expect("oversized");
        let config = test_config(temp_dir.clone());

        let preview = preview_workspace_file(&config, Some("docs/guide.md")).expect("preview");

        assert_eq!(preview.path, "docs/guide.md");
        assert_eq!(preview.content, "# Guide");
        assert!(!preview.truncated);
        assert!(preview_workspace_file(&config, Some("docs")).is_err());
        assert!(preview_workspace_file(&config, Some("image.bin")).is_err());
        assert!(preview_workspace_file(&config, Some("huge.txt")).is_err());

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

    #[test]
    fn rename_conflict_returns_typed_workspace_error() {
        let temp_dir =
            std::env::temp_dir().join(format!("soma-workspace-change-{}", Uuid::new_v4()));
        std::fs::create_dir_all(temp_dir.join("docs")).expect("docs dir");
        std::fs::write(temp_dir.join("docs").join("source.md"), "# Source").expect("source");
        std::fs::write(temp_dir.join("docs").join("existing.md"), "# Existing").expect("existing");
        let config = test_config(temp_dir.clone());
        let input = WorkspaceFileChangePreviewRequest {
            action: WorkspaceFileChangeAction::RenamePath,
            path: "docs/source.md".to_string(),
            target_path: Some("docs/existing.md".to_string()),
            content: None,
            expected_modified_at_ms: None,
        };

        let error = preview_workspace_file_change(&config, &input, Uuid::new_v4(), 1)
            .expect_err("rename conflict");

        assert_eq!(error.kind(), WorkspaceErrorKind::Conflict);
        assert_eq!(error.api_code(), "workspace_conflict");
        assert_eq!(error.status_code(), 409);

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

    #[test]
    fn stale_preview_returns_typed_workspace_file_stale_error() {
        let temp_dir =
            std::env::temp_dir().join(format!("soma-workspace-stale-{}", Uuid::new_v4()));
        std::fs::create_dir_all(temp_dir.join("docs")).expect("docs dir");
        std::fs::write(temp_dir.join("docs").join("guide.md"), "# Guide").expect("guide");
        let config = test_config(temp_dir.clone());
        let input = WorkspaceFileChangePreviewRequest {
            action: WorkspaceFileChangeAction::WriteText,
            path: "docs/guide.md".to_string(),
            target_path: None,
            content: Some("# Updated".to_string()),
            expected_modified_at_ms: Some(u64::MAX),
        };

        let error = preview_workspace_file_change(&config, &input, Uuid::new_v4(), 1)
            .expect_err("stale preview");

        assert_eq!(error.kind(), WorkspaceErrorKind::FileStale);
        assert_eq!(error.api_code(), "workspace_file_stale");
        assert_eq!(error.status_code(), 409);

        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,
        }
    }
}
