use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::UNIX_EPOCH;

use a3s_boot::{BootError, Result as BootResult};
use serde_json::{json, Value};
use tokio::fs;

use crate::api::code_web::state::CodeWebState;

pub(in crate::api::code_web) struct WorkspaceSearchOptions {
    pub(in crate::api::code_web) case_sensitive: bool,
    pub(in crate::api::code_web) use_regex: bool,
    pub(in crate::api::code_web) match_whole_word: bool,
    pub(in crate::api::code_web) include_pattern: Option<String>,
    pub(in crate::api::code_web) exclude_pattern: Option<String>,
    pub(in crate::api::code_web) max_results: usize,
}

pub(in crate::api::code_web) struct WorkspaceService {
    state: Arc<CodeWebState>,
}

impl WorkspaceService {
    pub(in crate::api::code_web) fn new(state: Arc<CodeWebState>) -> Self {
        Self { state }
    }

    pub(in crate::api::code_web) fn default_root(&self) -> serde_json::Value {
        json!({
            "root": self.state.default_workspace.display().to_string(),
        })
    }

    pub(in crate::api::code_web) async fn inspect_readiness(
        &self,
        workspace_root: Option<String>,
        repair: bool,
    ) -> BootResult<serde_json::Value> {
        let root = workspace_root
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(expand_home)
            .unwrap_or_else(|| self.state.default_workspace.clone());
        let agents_dir = root.join("agents");
        let sessions_dir = root.join("sessions");

        if repair {
            fs::create_dir_all(&agents_dir).await.map_err(fs_error)?;
            fs::create_dir_all(&sessions_dir).await.map_err(fs_error)?;
        }

        let root_exists = path_exists(&root).await;
        let agents_exists = path_exists(&agents_dir).await;
        let sessions_exists = path_exists(&sessions_dir).await;

        Ok(json!({
            "workspaceRoot": root.display().to_string(),
            "rootExists": root_exists,
            "agentsExists": agents_exists,
            "sessionsExists": sessions_exists,
            "needsRepair": !(root_exists && agents_exists && sessions_exists),
            "platform": std::env::consts::OS,
            "isWindows": cfg!(windows),
        }))
    }

    pub(in crate::api::code_web) async fn init_agent(
        &self,
        request: Value,
    ) -> BootResult<serde_json::Value> {
        let workspace = required_json_path(&request, "workspacePath")?;
        fs::create_dir_all(workspace.join(".a3s"))
            .await
            .map_err(fs_error)?;
        Ok(json!({ "success": true }))
    }

    pub(in crate::api::code_web) async fn init_prompt(
        &self,
        request: Value,
    ) -> BootResult<serde_json::Value> {
        let workspace = optional_json_path(&request, "workspace")
            .or_else(|| optional_json_path(&request, "workspacePath"))
            .transpose()?
            .unwrap_or_else(|| self.state.default_workspace.clone());
        let agents_path = workspace.join("AGENTS.md");
        Ok(json!({
            "workspace": workspace.display().to_string(),
            "path": agents_path.display().to_string(),
            "exists": agents_path.is_file(),
            "display": "/init - generate AGENTS.md",
            "prompt": init_agents_prompt(&workspace),
        }))
    }

    pub(in crate::api::code_web) async fn create_dir(
        &self,
        request: Value,
    ) -> BootResult<serde_json::Value> {
        let path = required_json_path(&request, "path")?;
        fs::create_dir_all(path).await.map_err(fs_error)?;
        Ok(json!({ "success": true }))
    }

    pub(in crate::api::code_web) async fn write_file(
        &self,
        request: Value,
    ) -> BootResult<serde_json::Value> {
        let path = required_json_path(&request, "path")?;
        let content = request
            .get("content")
            .and_then(Value::as_str)
            .unwrap_or_default();
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).await.map_err(fs_error)?;
        }
        fs::write(path, content).await.map_err(fs_error)?;
        Ok(json!({ "success": true }))
    }

    pub(in crate::api::code_web) async fn write_binary_file(
        &self,
        request: Value,
    ) -> BootResult<serde_json::Value> {
        let path = required_json_path(&request, "path")?;
        let bytes = request
            .get("data")
            .and_then(Value::as_array)
            .ok_or_else(|| BootError::BadRequest("data is required".to_string()))?
            .iter()
            .map(|value| {
                value
                    .as_u64()
                    .filter(|byte| *byte <= u8::MAX as u64)
                    .map(|byte| byte as u8)
                    .ok_or_else(|| {
                        BootError::BadRequest("data must contain byte values".to_string())
                    })
            })
            .collect::<BootResult<Vec<u8>>>()?;
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).await.map_err(fs_error)?;
        }
        fs::write(path, bytes).await.map_err(fs_error)?;
        Ok(json!({ "success": true }))
    }

    pub(in crate::api::code_web) async fn read_file(
        &self,
        path: String,
    ) -> BootResult<serde_json::Value> {
        let path = required_path(path)?;
        let content = fs::read_to_string(path).await.map_err(fs_error)?;
        Ok(json!({ "content": content }))
    }

    pub(in crate::api::code_web) async fn read_binary_file(
        &self,
        path: String,
    ) -> BootResult<Vec<u8>> {
        let path = required_path(path)?;
        fs::read(path).await.map_err(fs_error)
    }

    pub(in crate::api::code_web) async fn path_exists(
        &self,
        path: String,
    ) -> BootResult<serde_json::Value> {
        let path = required_path(path)?;
        Ok(json!({ "exists": path_exists(&path).await }))
    }

    pub(in crate::api::code_web) async fn delete_path(
        &self,
        path: String,
    ) -> BootResult<serde_json::Value> {
        let path = required_path(path)?;
        let metadata = fs::metadata(&path).await.map_err(fs_error)?;
        if metadata.is_dir() {
            fs::remove_dir_all(path).await.map_err(fs_error)?;
        } else {
            fs::remove_file(path).await.map_err(fs_error)?;
        }
        Ok(json!({ "success": true }))
    }

    pub(in crate::api::code_web) async fn read_dir(
        &self,
        path: String,
    ) -> BootResult<Vec<serde_json::Value>> {
        let path = required_path(path)?;
        let mut entries = fs::read_dir(path).await.map_err(fs_error)?;
        let mut items = Vec::new();
        while let Some(entry) = entries.next_entry().await.map_err(fs_error)? {
            let metadata = entry.metadata().await.map_err(fs_error)?;
            let name = entry.file_name().to_string_lossy().to_string();
            let modified_at = metadata.modified().ok().and_then(|time| {
                time.duration_since(UNIX_EPOCH)
                    .ok()
                    .map(|duration| duration.as_millis() as u64)
            });
            items.push(json!({
                "name": name,
                "isDirectory": metadata.is_dir(),
                "isFile": metadata.is_file(),
                "size": metadata.len(),
                "mtimeMs": modified_at,
                "extension": entry.path().extension().and_then(|value| value.to_str()),
                "isBinary": false,
            }));
        }
        items.sort_by(|left, right| {
            let left_dir = left
                .get("isDirectory")
                .and_then(Value::as_bool)
                .unwrap_or(false);
            let right_dir = right
                .get("isDirectory")
                .and_then(Value::as_bool)
                .unwrap_or(false);
            right_dir
                .cmp(&left_dir)
                .then_with(|| value_name(left).cmp(value_name(right)))
        });
        Ok(items)
    }

    pub(in crate::api::code_web) async fn rename_path(
        &self,
        request: Value,
    ) -> BootResult<serde_json::Value> {
        let src = required_json_path(&request, "src")?;
        let dest = required_json_path(&request, "dest")?;
        if let Some(parent) = dest.parent() {
            fs::create_dir_all(parent).await.map_err(fs_error)?;
        }
        fs::rename(src, dest).await.map_err(fs_error)?;
        Ok(json!({ "success": true }))
    }

    pub(in crate::api::code_web) async fn copy_path(
        &self,
        request: Value,
    ) -> BootResult<serde_json::Value> {
        let src = required_json_path(&request, "src")?;
        let dest = required_json_path(&request, "dest")?;
        copy_path(&src, &dest).await?;
        Ok(json!({ "success": true }))
    }

    pub(in crate::api::code_web) fn git_status(
        &self,
        root_path: Option<String>,
    ) -> serde_json::Value {
        let root = root_path
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(expand_home)
            .unwrap_or_else(|| self.state.default_workspace.clone());
        let is_git_repo = root.join(".git").exists();
        json!({
            "isGitRepo": is_git_repo,
            "branch": git_branch(&root),
            "files": [],
        })
    }

    pub(in crate::api::code_web) async fn search_files(
        &self,
        root_path: String,
        query: String,
        options: WorkspaceSearchOptions,
    ) -> BootResult<Vec<serde_json::Value>> {
        if options.use_regex {
            return Err(BootError::BadRequest(
                "regex search is not supported by the local web API yet".to_string(),
            ));
        }
        let query = query.trim().to_string();
        if query.is_empty() {
            return Ok(Vec::new());
        }
        let root = required_path(root_path)?;
        let files = collect_text_candidate_files(
            &root,
            options.include_pattern.as_deref(),
            options.exclude_pattern.as_deref(),
        )
        .await?;
        let mut results = Vec::new();
        let mut total_matches = 0usize;

        for file in files {
            if total_matches >= options.max_results.max(1) {
                break;
            }
            let Ok(content) = fs::read_to_string(&file).await else {
                continue;
            };
            let mut matches = Vec::new();
            for (line_index, line) in content.lines().enumerate() {
                for (start, end) in find_line_matches(
                    line,
                    &query,
                    options.case_sensitive,
                    options.match_whole_word,
                ) {
                    matches.push(json!({
                        "line": line_index + 1,
                        "column": start + 1,
                        "text": line,
                        "matchStart": start,
                        "matchEnd": end,
                    }));
                    total_matches += 1;
                    if total_matches >= options.max_results.max(1) {
                        break;
                    }
                }
                if total_matches >= options.max_results.max(1) {
                    break;
                }
            }
            if !matches.is_empty() {
                results.push(json!({
                    "path": file.display().to_string(),
                    "matches": matches,
                }));
            }
        }

        Ok(results)
    }

    pub(in crate::api::code_web) async fn replace_in_files(
        &self,
        request: Value,
    ) -> BootResult<serde_json::Value> {
        let root = required_json_path(&request, "rootPath")?;
        let query = request
            .get("query")
            .and_then(Value::as_str)
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .ok_or_else(|| BootError::BadRequest("query is required".to_string()))?;
        let replacement = request
            .get("replacement")
            .and_then(Value::as_str)
            .unwrap_or_default();
        let use_regex = request
            .get("useRegex")
            .and_then(Value::as_bool)
            .unwrap_or(false);
        if use_regex {
            return Err(BootError::BadRequest(
                "regex replace is not supported by the local web API yet".to_string(),
            ));
        }
        let options = WorkspaceSearchOptions {
            case_sensitive: request
                .get("caseSensitive")
                .and_then(Value::as_bool)
                .unwrap_or(false),
            use_regex,
            match_whole_word: request
                .get("matchWholeWord")
                .and_then(Value::as_bool)
                .unwrap_or(false),
            include_pattern: request
                .get("includePattern")
                .and_then(Value::as_str)
                .map(str::to_string),
            exclude_pattern: request
                .get("excludePattern")
                .and_then(Value::as_str)
                .map(str::to_string),
            max_results: usize::MAX,
        };
        let files = if let Some(file_paths) = request.get("filePaths").and_then(Value::as_array) {
            file_paths
                .iter()
                .filter_map(Value::as_str)
                .map(|path| required_path(path.to_string()))
                .collect::<BootResult<Vec<_>>>()?
        } else {
            collect_text_candidate_files(
                &root,
                options.include_pattern.as_deref(),
                options.exclude_pattern.as_deref(),
            )
            .await?
        };
        let mut modified_files = Vec::new();
        let mut total_replacements = 0usize;

        for file in files {
            let Ok(content) = fs::read_to_string(&file).await else {
                continue;
            };
            let (next_content, replacements) = replace_text(
                &content,
                query,
                replacement,
                options.case_sensitive,
                options.match_whole_word,
            );
            if replacements == 0 {
                continue;
            }
            fs::write(&file, next_content).await.map_err(fs_error)?;
            total_replacements += replacements;
            modified_files.push(json!({
                "path": file.display().to_string(),
                "replacements": replacements,
            }));
        }

        Ok(json!({
            "filesModified": modified_files.len(),
            "totalReplacements": total_replacements,
            "files": modified_files,
        }))
    }
}

async fn path_exists(path: &Path) -> bool {
    fs::metadata(path).await.is_ok()
}

fn required_json_path(value: &Value, field: &str) -> BootResult<PathBuf> {
    let raw = value
        .get(field)
        .and_then(Value::as_str)
        .ok_or_else(|| BootError::BadRequest(format!("{field} is required")))?;
    required_path(raw.to_string())
}

fn optional_json_path(value: &Value, field: &str) -> Option<BootResult<PathBuf>> {
    value
        .get(field)
        .and_then(Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(|value| required_path(value.to_string()))
}

fn init_agents_prompt(workspace: &Path) -> String {
    let agents_path = workspace.join("AGENTS.md");
    format!(
        "Analyze this codebase at `{workspace}` and create (or update) an AGENTS.md file at `{agents_path}`. \
         Include: a concise project overview, the exact build / test / lint / run commands, \
         the high-level architecture and key directories, and the conventions an AI coding agent should follow. \
         Base everything on what's actually in the workspace, and write the file with your file-writing tool.",
        workspace = workspace.display(),
        agents_path = agents_path.display(),
    )
}

fn required_path(value: String) -> BootResult<PathBuf> {
    let trimmed = value.trim();
    if trimmed.is_empty() {
        return Err(BootError::BadRequest("path is required".to_string()));
    }
    Ok(expand_home(trimmed))
}

fn expand_home(path: &str) -> PathBuf {
    if let Some(rest) = path.strip_prefix("~/") {
        if let Some(home) = std::env::var_os("HOME") {
            return Path::new(&home).join(rest);
        }
    }
    PathBuf::from(path)
}

fn fs_error(error: std::io::Error) -> BootError {
    match error.kind() {
        std::io::ErrorKind::NotFound => BootError::NotFound(error.to_string()),
        std::io::ErrorKind::PermissionDenied => BootError::Forbidden(error.to_string()),
        std::io::ErrorKind::InvalidInput | std::io::ErrorKind::InvalidData => {
            BootError::BadRequest(error.to_string())
        }
        _ => BootError::Io(error),
    }
}

fn value_name(value: &Value) -> &str {
    value.get("name").and_then(Value::as_str).unwrap_or("")
}

async fn copy_path(src: &Path, dest: &Path) -> BootResult<()> {
    let src = src.to_path_buf();
    let dest = dest.to_path_buf();
    tokio::task::spawn_blocking(move || copy_path_sync(&src, &dest))
        .await
        .map_err(|error| BootError::Internal(error.to_string()))?
        .map_err(fs_error)
}

fn copy_path_sync(src: &Path, dest: &Path) -> std::io::Result<()> {
    let metadata = std::fs::metadata(src)?;
    if metadata.is_dir() {
        std::fs::create_dir_all(dest)?;
        for entry in std::fs::read_dir(src)? {
            let entry = entry?;
            copy_path_sync(&entry.path(), &dest.join(entry.file_name()))?;
        }
    } else {
        if let Some(parent) = dest.parent() {
            std::fs::create_dir_all(parent)?;
        }
        std::fs::copy(src, dest)?;
    }
    Ok(())
}

fn git_branch(root: &Path) -> Option<String> {
    let head = std::fs::read_to_string(root.join(".git/HEAD")).ok()?;
    let head = head.trim();
    head.strip_prefix("ref: refs/heads/")
        .map(str::to_string)
        .or_else(|| (!head.is_empty()).then(|| head.to_string()))
}

async fn collect_text_candidate_files(
    root: &Path,
    include_pattern: Option<&str>,
    exclude_pattern: Option<&str>,
) -> BootResult<Vec<PathBuf>> {
    let root = root.to_path_buf();
    let include_pattern = include_pattern.map(str::to_string);
    let exclude_pattern = exclude_pattern.map(str::to_string);
    tokio::task::spawn_blocking(move || {
        let mut files = Vec::new();
        collect_files_sync(
            &root,
            &root,
            include_pattern.as_deref(),
            exclude_pattern.as_deref(),
            &mut files,
        )?;
        Ok(files)
    })
    .await
    .map_err(|error| BootError::Internal(error.to_string()))?
}

fn collect_files_sync(
    root: &Path,
    current: &Path,
    include_pattern: Option<&str>,
    exclude_pattern: Option<&str>,
    files: &mut Vec<PathBuf>,
) -> BootResult<()> {
    for entry in std::fs::read_dir(current).map_err(fs_error)? {
        let entry = entry.map_err(fs_error)?;
        let path = entry.path();
        let metadata = entry.metadata().map_err(fs_error)?;
        if metadata.is_dir() {
            collect_files_sync(root, &path, include_pattern, exclude_pattern, files)?;
        } else if metadata.is_file() {
            let relative = path
                .strip_prefix(root)
                .unwrap_or(&path)
                .to_string_lossy()
                .replace('\\', "/");
            if path_is_included(&relative, include_pattern)
                && !path_is_excluded(&relative, exclude_pattern)
                && looks_like_text_path(&path)
            {
                files.push(path);
            }
        }
    }
    Ok(())
}

fn looks_like_text_path(path: &Path) -> bool {
    match path.extension().and_then(|value| value.to_str()) {
        None => true,
        Some(ext) => matches!(
            ext.to_ascii_lowercase().as_str(),
            "acl"
                | "bash"
                | "c"
                | "css"
                | "csv"
                | "env"
                | "h"
                | "hcl"
                | "html"
                | "js"
                | "json"
                | "jsx"
                | "md"
                | "mdx"
                | "py"
                | "rs"
                | "sh"
                | "toml"
                | "ts"
                | "tsx"
                | "txt"
                | "xml"
                | "yaml"
                | "yml"
        ),
    }
}

fn path_is_included(path: &str, pattern: Option<&str>) -> bool {
    let Some(pattern) = pattern.map(str::trim).filter(|value| !value.is_empty()) else {
        return true;
    };
    path_matches_patterns(path, pattern)
}

fn path_is_excluded(path: &str, pattern: Option<&str>) -> bool {
    let Some(pattern) = pattern.map(str::trim).filter(|value| !value.is_empty()) else {
        return false;
    };
    path_matches_patterns(path, pattern)
}

fn path_matches_patterns(path: &str, pattern: &str) -> bool {
    pattern
        .split(',')
        .map(str::trim)
        .filter(|part| !part.is_empty())
        .any(|part| wildcard_match(path, part) || path.contains(part))
}

fn wildcard_match(path: &str, pattern: &str) -> bool {
    if pattern == "*" {
        return true;
    }
    if let Some(suffix) = pattern.strip_prefix("*.") {
        return path
            .rsplit('/')
            .next()
            .is_some_and(|name| name.ends_with(&format!(".{suffix}")));
    }
    if !pattern.contains('*') {
        return path == pattern;
    }
    let parts: Vec<&str> = pattern.split('*').collect();
    let mut cursor = 0usize;
    for (index, part) in parts.iter().enumerate() {
        if part.is_empty() {
            continue;
        }
        let Some(found) = path[cursor..].find(part) else {
            return false;
        };
        if index == 0 && !pattern.starts_with('*') && found != 0 {
            return false;
        }
        cursor += found + part.len();
    }
    pattern.ends_with('*') || parts.last().is_none_or(|last| path.ends_with(last))
}

fn find_line_matches(
    line: &str,
    query: &str,
    case_sensitive: bool,
    match_whole_word: bool,
) -> Vec<(usize, usize)> {
    let haystack = if case_sensitive {
        line.to_string()
    } else {
        line.to_lowercase()
    };
    let needle = if case_sensitive {
        query.to_string()
    } else {
        query.to_lowercase()
    };
    if needle.is_empty() {
        return Vec::new();
    }
    let mut matches = Vec::new();
    let mut cursor = 0usize;
    while cursor <= haystack.len() {
        let Some(offset) = haystack[cursor..].find(&needle) else {
            break;
        };
        let start = cursor + offset;
        let end = start + needle.len();
        if !match_whole_word || is_whole_word(line, start, end) {
            matches.push((start, end));
        }
        cursor = end.max(start + 1);
    }
    matches
}

fn replace_text(
    content: &str,
    query: &str,
    replacement: &str,
    case_sensitive: bool,
    match_whole_word: bool,
) -> (String, usize) {
    let mut output = String::with_capacity(content.len());
    let mut replacements = 0usize;
    for (line_index, line) in content.split_inclusive('\n').enumerate() {
        let line_body = line.strip_suffix('\n').unwrap_or(line);
        let newline = if line.ends_with('\n') { "\n" } else { "" };
        let matches = find_line_matches(line_body, query, case_sensitive, match_whole_word);
        if matches.is_empty() {
            output.push_str(line_body);
            output.push_str(newline);
            continue;
        }
        let mut cursor = 0usize;
        for (start, end) in matches {
            output.push_str(&line_body[cursor..start]);
            output.push_str(replacement);
            cursor = end;
            replacements += 1;
        }
        output.push_str(&line_body[cursor..]);
        output.push_str(newline);
        if line_index == 0 && content.is_empty() {
            break;
        }
    }
    if content.is_empty() {
        (content.to_string(), 0)
    } else {
        (output, replacements)
    }
}

fn is_whole_word(line: &str, start: usize, end: usize) -> bool {
    let before = line[..start].chars().next_back();
    let after = line[end..].chars().next();
    !before.is_some_and(is_word_char) && !after.is_some_and(is_word_char)
}

fn is_word_char(value: char) -> bool {
    value.is_ascii_alphanumeric() || value == '_'
}

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

    #[test]
    fn init_agents_prompt_matches_tui_init_contract() {
        let workspace = PathBuf::from("/tmp/a3s-web-init");
        let prompt = init_agents_prompt(&workspace);

        assert!(prompt.contains("/tmp/a3s-web-init"));
        assert!(prompt.contains("AGENTS.md"));
        assert!(prompt.contains("build / test / lint / run commands"));
        assert!(prompt.contains("AI coding agent should follow"));
        assert!(prompt.contains("file-writing tool"));
    }
}
