use serde::Serialize;
use serde_json::Value;
use std::sync::Mutex;
use std::{
    fs,
    io::{Read, Write},
    net::{SocketAddr, TcpStream},
    path::{Path, PathBuf},
    process::Command,
    time::Duration,
};
use tauri::{AppHandle, Manager};

const DEFAULT_PORT: u16 = 5177;
const PORT_SCAN_LIMIT: u16 = 100;
static LAUNCH_LOCK: Mutex<()> = Mutex::new(());

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ReviewEditorLaunchResponse {
    ok: bool,
    url: String,
    reused: bool,
    review_root: String,
    version: String,
    session_id: Option<String>,
}

struct CommandOutput {
    stdout: String,
    stderr: String,
}

struct PythonCommand {
    program: String,
    args: Vec<String>,
}

#[tauri::command]
pub async fn launch_epub_review_editor(
    app: AppHandle,
    epub_path: Option<String>,
) -> Result<ReviewEditorLaunchResponse, String> {
    tauri::async_runtime::spawn_blocking(move || launch_epub_review_editor_sync(&app, epub_path))
        .await
        .map_err(|err| err.to_string())?
}

fn launch_epub_review_editor_sync(
    app: &AppHandle,
    epub_path: Option<String>,
) -> Result<ReviewEditorLaunchResponse, String> {
    let _launch_guard = LAUNCH_LOCK
        .lock()
        .map_err(|_| "审校器启动锁已损坏。请重启 Readest 后重试。".to_string())?;
    let tool_root = resolve_tool_root(app)?;
    let server_path = tool_root.join("server.py");
    if !server_path.exists() {
        return Err(format!(
            "没有找到审校器后端：{}",
            server_path.to_string_lossy()
        ));
    }

    let review_root = std::env::var("READEST_REVIEW_ROOT")
        .map(PathBuf::from)
        .unwrap_or_else(|_| {
            app.path()
                .app_data_dir()
                .unwrap_or_else(|_| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")))
                .join("epub_review_sessions")
        });
    fs::create_dir_all(&review_root).map_err(|err| {
        format!(
            "创建审校数据目录失败：{} ({err})",
            review_root.to_string_lossy()
        )
    })?;

    let expected_version = read_expected_version(&tool_root);
    if let Some(url) = find_running_editor(expected_version.as_deref(), &review_root) {
        let session_id = ensure_session_from_path(&url, epub_path.as_deref())?;
        return Ok(ReviewEditorLaunchResponse {
            ok: true,
            url,
            reused: true,
            review_root: review_root.to_string_lossy().to_string(),
            version: expected_version.unwrap_or_default(),
            session_id,
        });
    }

    let runtime_root = app
        .path()
        .app_data_dir()
        .unwrap_or_else(|_| review_root.clone())
        .join("epub-review-editor-runtime");
    let venv_root = runtime_root.join(".venv");
    let venv_python = venv_python_path(&venv_root);

    ensure_venv(&venv_python, &venv_root)?;
    ensure_dependencies(&venv_python, &tool_root)?;

    let output = run_command(
        &venv_python.to_string_lossy(),
        &[
            server_path.to_string_lossy().to_string(),
            "--review-root".to_string(),
            review_root.to_string_lossy().to_string(),
            "--host".to_string(),
            "127.0.0.1".to_string(),
            "--port".to_string(),
            DEFAULT_PORT.to_string(),
            "--daemon".to_string(),
            "--no-browser".to_string(),
        ],
        Some(&tool_root),
    )?;

    let launched_url = extract_url(&output.stdout)
        .or_else(|| find_running_editor(expected_version.as_deref(), &review_root))
        .ok_or_else(|| {
            format!(
                "审校器已启动但没有返回访问地址。\nstdout:\n{}\nstderr:\n{}",
                output.stdout, output.stderr
            )
        })?;

    let launched_url = launched_url.replace("http://localhost:", "http://127.0.0.1:");
    let session_id = ensure_session_from_path(&launched_url, epub_path.as_deref())?;

    Ok(ReviewEditorLaunchResponse {
        ok: true,
        url: launched_url,
        reused: false,
        review_root: review_root.to_string_lossy().to_string(),
        version: expected_version.unwrap_or_default(),
        session_id,
    })
}

fn resolve_tool_root(app: &AppHandle) -> Result<PathBuf, String> {
    if let Ok(raw) = std::env::var("READEST_REVIEW_EDITOR_TOOL_ROOT") {
        let path = PathBuf::from(raw);
        if path.join("server.py").exists() {
            return Ok(path);
        }
    }

    if let Ok(resource_dir) = app.path().resource_dir() {
        for candidate in [
            resource_dir.join("tools").join("epub-review-editor"),
            resource_dir
                .join("apps")
                .join("readest-app")
                .join("tools")
                .join("epub-review-editor"),
        ] {
            if candidate.join("server.py").exists() {
                return Ok(candidate);
            }
        }
    }

    #[cfg(debug_assertions)]
    if let Ok(current) = std::env::current_dir() {
        for ancestor in current.ancestors() {
            for candidate in [
                ancestor.join("tools").join("epub-review-editor"),
                ancestor
                    .join("apps")
                    .join("readest-app")
                    .join("tools")
                    .join("epub-review-editor"),
            ] {
                if candidate.join("server.py").exists() {
                    return Ok(candidate);
                }
            }
        }
    }

    Err("没有找到已迁移的 EPUB 审校器资源目录。".to_string())
}

fn ensure_venv(venv_python: &Path, venv_root: &Path) -> Result<(), String> {
    if venv_python.exists() {
        return Ok(());
    }
    if let Some(parent) = venv_root.parent() {
        fs::create_dir_all(parent).map_err(|err| {
            format!(
                "创建审校器运行目录失败：{} ({err})",
                parent.to_string_lossy()
            )
        })?;
    }
    let python = resolve_python_command()?;
    let mut args = python.args;
    args.extend([
        "-m".to_string(),
        "venv".to_string(),
        venv_root.to_string_lossy().to_string(),
    ]);
    run_command(&python.program, &args, None)?;
    Ok(())
}

fn ensure_dependencies(venv_python: &Path, tool_root: &Path) -> Result<(), String> {
    let check = run_command_allow_failure(
        &venv_python.to_string_lossy(),
        &[
            "-c".to_string(),
            "import importlib.util, sys; sys.exit(0 if importlib.util.find_spec('flask') else 1)"
                .to_string(),
        ],
        Some(tool_root),
    )?;
    if check.0 {
        return Ok(());
    }

    let requirements = tool_root.join("requirements.txt");
    run_command(
        &venv_python.to_string_lossy(),
        &[
            "-m".to_string(),
            "pip".to_string(),
            "install".to_string(),
            "-r".to_string(),
            requirements.to_string_lossy().to_string(),
        ],
        Some(tool_root),
    )?;
    Ok(())
}

fn venv_python_path(venv_root: &Path) -> PathBuf {
    if cfg!(windows) {
        venv_root.join("Scripts").join("python.exe")
    } else {
        venv_root.join("bin").join("python")
    }
}

fn resolve_python_command() -> Result<PythonCommand, String> {
    let candidates: Vec<PythonCommand> = if cfg!(windows) {
        vec![
            PythonCommand {
                program: "py".to_string(),
                args: vec!["-3".to_string()],
            },
            PythonCommand {
                program: "python".to_string(),
                args: vec![],
            },
            PythonCommand {
                program: "python3".to_string(),
                args: vec![],
            },
        ]
    } else {
        vec![
            PythonCommand {
                program: "python3".to_string(),
                args: vec![],
            },
            PythonCommand {
                program: "python".to_string(),
                args: vec![],
            },
        ]
    };

    for candidate in candidates {
        let mut args = candidate.args.clone();
        args.push("--version".to_string());
        if run_command_allow_failure(&candidate.program, &args, None)
            .map(|(ok, _)| ok)
            .unwrap_or(false)
        {
            return Ok(candidate);
        }
    }
    Err("没有找到 Python 3。请先安装 Python 3，然后重新打开 EPUB 审校器。".to_string())
}

fn read_expected_version(tool_root: &Path) -> Option<String> {
    let text = fs::read_to_string(tool_root.join("version.py")).ok()?;
    let marker = "version = \"";
    let start = text.find(marker)? + marker.len();
    let rest = &text[start..];
    Some(rest[..rest.find('"')?].to_string())
}

fn find_running_editor(expected_version: Option<&str>, review_root: &Path) -> Option<String> {
    let expected_root = review_root
        .canonicalize()
        .unwrap_or_else(|_| review_root.to_path_buf());
    for port in DEFAULT_PORT..(DEFAULT_PORT + PORT_SCAN_LIMIT) {
        let Some(payload) = request_json(port, "/api/version") else {
            continue;
        };
        let Some(version) = payload.get("version").and_then(Value::as_str) else {
            continue;
        };
        if expected_version.map_or(true, |expected| expected == version) {
            let running_root = payload
                .get("review_root")
                .and_then(Value::as_str)
                .map(PathBuf::from);
            let running_root_matches = running_root
                .as_ref()
                .and_then(|path| path.canonicalize().ok())
                .map(|path| path == expected_root)
                .unwrap_or(false);
            if !running_root_matches {
                continue;
            }
            return Some(format!("http://127.0.0.1:{port}"));
        }
    }
    None
}

fn request_json(port: u16, path: &str) -> Option<Value> {
    let addr = SocketAddr::from(([127, 0, 0, 1], port));
    let mut stream = TcpStream::connect_timeout(&addr, Duration::from_millis(250)).ok()?;
    let _ = stream.set_read_timeout(Some(Duration::from_millis(800)));
    let _ = stream.set_write_timeout(Some(Duration::from_millis(800)));
    let request = format!("GET {path} HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n");
    stream.write_all(request.as_bytes()).ok()?;
    read_http_json_response(stream)
}

fn post_json(url: &str, path: &str, body: &str) -> Result<Value, String> {
    let port = parse_loopback_port(url)?;
    let addr = SocketAddr::from(([127, 0, 0, 1], port));
    let mut stream = TcpStream::connect_timeout(&addr, Duration::from_secs(3))
        .map_err(|err| format!("连接审校器失败：{err}"))?;
    let _ = stream.set_read_timeout(Some(Duration::from_secs(30)));
    let _ = stream.set_write_timeout(Some(Duration::from_secs(5)));
    let request = format!(
        "POST {path} HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
        body.len(),
        body
    );
    stream
        .write_all(request.as_bytes())
        .map_err(|err| format!("写入审校器请求失败：{err}"))?;
    read_http_json_response_result(stream)
}

fn read_http_json_response(mut stream: TcpStream) -> Option<Value> {
    let mut response = String::new();
    stream.read_to_string(&mut response).ok()?;
    if !response.starts_with("HTTP/1.1 200") && !response.starts_with("HTTP/1.0 200") {
        return None;
    }
    let body = response.split("\r\n\r\n").nth(1)?;
    serde_json::from_str(body).ok()
}

fn read_http_json_response_result(mut stream: TcpStream) -> Result<Value, String> {
    let mut response = String::new();
    stream
        .read_to_string(&mut response)
        .map_err(|err| format!("读取审校器响应失败：{err}"))?;
    let status_line = response.lines().next().unwrap_or_default().to_string();
    let body = response.split("\r\n\r\n").nth(1).unwrap_or("");
    let payload = serde_json::from_str::<Value>(body).unwrap_or(Value::Null);
    if status_line.starts_with("HTTP/1.1 200") || status_line.starts_with("HTTP/1.0 200") {
        if payload.is_null() {
            return Err("审校器返回了空响应。".to_string());
        }
        return Ok(payload);
    }
    let message = payload
        .get("error")
        .and_then(Value::as_str)
        .unwrap_or(status_line.as_str());
    Err(message.to_string())
}

fn parse_loopback_port(url: &str) -> Result<u16, String> {
    let rest = url
        .strip_prefix("http://127.0.0.1:")
        .or_else(|| url.strip_prefix("http://localhost:"))
        .ok_or_else(|| format!("审校器地址不是本机 HTTP 地址：{url}"))?;
    let port_text = rest.split('/').next().unwrap_or(rest);
    port_text
        .parse::<u16>()
        .map_err(|err| format!("无法解析审校器端口：{port_text} ({err})"))
}

fn ensure_session_from_path(url: &str, epub_path: Option<&str>) -> Result<Option<String>, String> {
    let Some(raw_path) = epub_path.map(str::trim).filter(|value| !value.is_empty()) else {
        return Ok(None);
    };
    let body = serde_json::json!({ "epub_path": raw_path, "activate": false }).to_string();
    let payload = post_json(url, "/api/session/from-path", &body)?;
    Ok(payload
        .get("id")
        .and_then(Value::as_str)
        .map(ToOwned::to_owned))
}

fn run_command(
    program: &str,
    args: &[String],
    cwd: Option<&Path>,
) -> Result<CommandOutput, String> {
    let (ok, output) = run_command_allow_failure(program, args, cwd)?;
    if ok {
        Ok(output)
    } else {
        Err(format!(
            "命令执行失败：{} {}\nstdout:\n{}\nstderr:\n{}",
            program,
            args.join(" "),
            output.stdout,
            output.stderr
        ))
    }
}

fn run_command_allow_failure(
    program: &str,
    args: &[String],
    cwd: Option<&Path>,
) -> Result<(bool, CommandOutput), String> {
    let mut command = Command::new(program);
    command.args(args);
    if let Some(cwd) = cwd {
        command.current_dir(cwd);
    }
    command.stdin(std::process::Stdio::null());
    hide_command_window(&mut command);
    let output = command
        .output()
        .map_err(|err| format!("无法执行命令 {program}: {err}"))?;
    Ok((
        output.status.success(),
        CommandOutput {
            stdout: String::from_utf8_lossy(&output.stdout).to_string(),
            stderr: String::from_utf8_lossy(&output.stderr).to_string(),
        },
    ))
}

#[cfg(windows)]
fn hide_command_window(command: &mut Command) {
    use std::os::windows::process::CommandExt;
    const CREATE_NO_WINDOW: u32 = 0x08000000;
    command.creation_flags(CREATE_NO_WINDOW);
}

#[cfg(not(windows))]
fn hide_command_window(_command: &mut Command) {}

fn extract_url(text: &str) -> Option<String> {
    text.split_whitespace()
        .find(|part| part.starts_with("http://") || part.starts_with("https://"))
        .map(ToOwned::to_owned)
}
