use std::env;
use std::fs;
use std::io::{self, BufRead, BufReader};
use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};

use crossterm::event::{self, Event, KeyCode};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
use crossterm::execute;
use ratatui::backend::CrosstermBackend;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Gauge, List, ListItem, Paragraph, Wrap};
use ratatui::Terminal;

use oker::FrameWork;

const MENU_ITEMS: [&str; 3] = ["New Project", "Start Server", "Quit"];
const FRAMEWORK_ITEMS: [(&str, FrameWork); 8] = [
    ("Svelte", FrameWork::Svelte),
    ("Vue", FrameWork::Vue),
    ("React", FrameWork::React),
    ("Astro", FrameWork::Astro),
    ("Lit", FrameWork::Lit),
    ("Quasar", FrameWork::Quasar),
    ("Alpine", FrameWork::Alpine),
    ("Vanilla", FrameWork::Vanilla),
];

fn main() -> io::Result<()> {
    let args: Vec<String> = env::args().collect();
    if args.len() >= 2 {
        match args[1].as_str() {
            "new" => {
                let mut name: Option<String> = None;
                let mut fw = FrameWork::Vanilla;
                let mut iter = args.iter().skip(2).peekable();
                while let Some(arg) = iter.next() {
                    if arg == "--framework" || arg == "-f" {
                        if let Some(value) = iter.next() {
                            if let Some(parsed) = parse_framework_name(value) {
                                fw = parsed;
                            }
                        }
                        continue;
                    }
                    if name.is_none() {
                        name = Some(arg.to_string());
                    }
                }

                let Some(name) = name else {
                    eprintln!("usage: okr new <project-name> [-f|--framework <name>]");
                    std::process::exit(1);
                };

                create_project(name.trim(), fw)?;
                println!("created {} ({})", name.trim(), fw.as_str());
                return Ok(());
            }
            "start" => {
                return run_tui(Mode::Running(start_running()?));
            }
            _ => {}
        }
    }

    run_tui(Mode::Menu)
}

enum Mode {
    Menu,
    NewName { input: String, error: Option<String> },
    NewFramework { name: String, index: usize },
    Running(RunningState),
}

struct RunningState {
    child: Option<Child>,
    log_rx: mpsc::Receiver<String>,
    logs: Vec<String>,
    start_time: Instant,
    status: String,
    done: bool,
}

struct AppState {
    mode: Mode,
    menu_index: usize,
    status: Option<String>,
    tick: u64,
}

fn run_tui(initial_mode: Mode) -> io::Result<()> {
    let mut stdout = io::stdout();
    enable_raw_mode()?;
    execute!(stdout, EnterAlternateScreen)?;
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;
    terminal.clear()?;

    let mut app = AppState {
        mode: initial_mode,
        menu_index: 0,
        status: None,
        tick: 0,
    };

    let result = loop {
        app.tick = app.tick.wrapping_add(1);
        terminal.draw(|frame| draw_ui(frame, &app))?;

        if event::poll(Duration::from_millis(60))? {
            if let Event::Key(key) = event::read()? {
                if handle_key(&mut app, key.code)? {
                    break Ok(());
                }
            }
        }

        if let Mode::Running(state) = &mut app.mode {
            drain_logs(state);
            if !state.done {
                if let Some(child) = state.child.as_mut() {
                    if let Some(status) = child.try_wait()? {
                        state.status = format!("exited: {}", status);
                        state.done = true;
                        state.child = None;
                    }
                }
            }
        }
    };

    disable_raw_mode()?;
    execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
    terminal.show_cursor()?;
    result
}

fn draw_ui(frame: &mut ratatui::Frame, app: &AppState) {
    let size = frame.size();
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Length(3), Constraint::Min(6), Constraint::Length(3)])
        .split(size);

    let header = Paragraph::new(Line::from(vec![
        Span::styled("okr", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
        Span::raw("  "),
        Span::styled("server helper", Style::default().fg(Color::Yellow)),
    ]))
    .block(Block::default().borders(Borders::ALL).title("Oker"));
    frame.render_widget(header, chunks[0]);

    match &app.mode {
        Mode::Menu => render_menu(frame, chunks[1], app.menu_index),
        Mode::NewName { input, error } => render_new_name(frame, chunks[1], input, error.as_deref()),
        Mode::NewFramework { name, index } => render_new_framework(frame, chunks[1], name, *index),
        Mode::Running(state) => render_running(frame, chunks[1], app.tick, state),
    }

    let footer_text = if let Some(status) = &app.status {
        status.clone()
    } else {
        match app.mode {
            Mode::Menu => "Up/Down: move  Enter: select  q: quit".to_string(),
            Mode::NewName { .. } => "type name  Enter: next  Esc: back".to_string(),
            Mode::NewFramework { .. } => "Up/Down: select  Enter: create  Esc: back".to_string(),
            Mode::Running(ref state) => {
                if state.done {
                    "done. Enter: back  q: quit".to_string()
                } else {
                    "q: stop  Esc: back".to_string()
                }
            }
        }
    };
    let footer = Paragraph::new(footer_text)
        .wrap(Wrap { trim: true })
        .block(Block::default().borders(Borders::ALL));
    frame.render_widget(footer, chunks[2]);
}

fn render_menu(frame: &mut ratatui::Frame, area: Rect, menu_index: usize) {
    let items: Vec<ListItem> = MENU_ITEMS
        .iter()
        .map(|item| ListItem::new(Line::from(Span::raw(*item))))
        .collect();
    let list = List::new(items)
        .block(Block::default().borders(Borders::ALL).title("Menu"))
        .highlight_style(
            Style::default()
                .fg(Color::Black)
                .bg(Color::Yellow)
                .add_modifier(Modifier::BOLD),
        )
        .highlight_symbol("> ");
    frame.render_stateful_widget(list, area, &mut list_state(menu_index));
}

fn render_new_name(frame: &mut ratatui::Frame, area: Rect, input: &str, error: Option<&str>) {
    let mut lines = vec![Line::from(vec![
        Span::styled("Project name: ", Style::default().fg(Color::Green)),
        Span::raw(input),
    ])];
    if let Some(err) = error {
        lines.push(Line::from(Span::styled(
            err,
            Style::default().fg(Color::Red),
        )));
    }
    let paragraph = Paragraph::new(lines)
        .block(Block::default().borders(Borders::ALL).title("New Project"));
    frame.render_widget(paragraph, area);
}

fn render_new_framework(frame: &mut ratatui::Frame, area: Rect, name: &str, index: usize) {
    let items: Vec<ListItem> = FRAMEWORK_ITEMS
        .iter()
        .map(|(label, _)| ListItem::new(Line::from(Span::raw(*label))))
        .collect();
    let list = List::new(items)
        .block(
            Block::default()
                .borders(Borders::ALL)
                .title(format!("Framework for {}", name)),
        )
        .highlight_style(
            Style::default()
                .fg(Color::Black)
                .bg(Color::Cyan)
                .add_modifier(Modifier::BOLD),
        )
        .highlight_symbol("> ");
    frame.render_stateful_widget(list, area, &mut list_state(index));
}

fn render_running(frame: &mut ratatui::Frame, area: Rect, tick: u64, state: &RunningState) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Length(3), Constraint::Min(5)])
        .split(area);

    let progress = ((tick % 100) as f64) / 100.0;
    let spinner = ["|", "/", "-", "\\"];
    let spin = spinner[(tick as usize) % spinner.len()];
    let title = format!("{} {}", spin, state.status);

    let gauge = Gauge::default()
        .block(Block::default().borders(Borders::ALL).title(title))
        .gauge_style(Style::default().fg(Color::LightGreen))
        .ratio(progress);
    frame.render_widget(gauge, chunks[0]);

    let logs: Vec<ListItem> = state
        .logs
        .iter()
        .rev()
        .take(200)
        .rev()
        .map(|line| ListItem::new(Line::from(Span::raw(line))))
        .collect();
    let list = List::new(logs)
        .block(Block::default().borders(Borders::ALL).title("Logs"));
    frame.render_widget(list, chunks[1]);
}

fn list_state(selected: usize) -> ratatui::widgets::ListState {
    let mut state = ratatui::widgets::ListState::default();
    state.select(Some(selected));
    state
}

fn handle_key(app: &mut AppState, code: KeyCode) -> io::Result<bool> {
    match app.mode {
        Mode::Menu => handle_menu_key(app, code),
        Mode::NewName { .. } => handle_new_name_key(app, code),
        Mode::NewFramework { .. } => handle_new_framework_key(app, code),
        Mode::Running(_) => handle_running_key(app, code),
    }
}

fn handle_menu_key(app: &mut AppState, code: KeyCode) -> io::Result<bool> {
    match code {
        KeyCode::Char('q') => return Ok(true),
        KeyCode::Down => {
            app.menu_index = (app.menu_index + 1) % MENU_ITEMS.len();
        }
        KeyCode::Up => {
            if app.menu_index == 0 {
                app.menu_index = MENU_ITEMS.len() - 1;
            } else {
                app.menu_index -= 1;
            }
        }
        KeyCode::Enter => match app.menu_index {
            0 => {
                app.mode = Mode::NewName {
                    input: String::new(),
                    error: None,
                };
                app.status = None;
            }
            1 => {
                match start_running() {
                    Ok(state) => {
                        app.mode = Mode::Running(state);
                        app.status = None;
                    }
                    Err(err) => {
                        app.status = Some(format!("start failed: {}", err));
                    }
                }
            }
            2 => return Ok(true),
            _ => {}
        },
        _ => {}
    }
    Ok(false)
}

fn handle_new_name_key(app: &mut AppState, code: KeyCode) -> io::Result<bool> {
    let Mode::NewName { input, error } = &mut app.mode else {
        return Ok(false);
    };
    match code {
        KeyCode::Esc => {
            app.mode = Mode::Menu;
            app.status = None;
        }
        KeyCode::Enter => {
            let name = input.trim();
            if name.is_empty() {
                *error = Some("name required".to_string());
                return Ok(false);
            }
            app.mode = Mode::NewFramework {
                name: name.to_string(),
                index: 0,
            };
        }
        KeyCode::Backspace => {
            input.pop();
        }
        KeyCode::Char(c) => {
            if !c.is_control() {
                input.push(c);
            }
        }
        _ => {}
    }
    Ok(false)
}

fn handle_new_framework_key(app: &mut AppState, code: KeyCode) -> io::Result<bool> {
    let Mode::NewFramework { name, index } = &mut app.mode else {
        return Ok(false);
    };
    match code {
        KeyCode::Esc => {
            app.mode = Mode::Menu;
            app.status = None;
        }
        KeyCode::Down => {
            *index = (*index + 1) % FRAMEWORK_ITEMS.len();
        }
        KeyCode::Up => {
            if *index == 0 {
                *index = FRAMEWORK_ITEMS.len() - 1;
            } else {
                *index -= 1;
            }
        }
        KeyCode::Enter => {
            let (label, fw) = FRAMEWORK_ITEMS[*index];
            match create_project(name, fw) {
                Ok(_) => {
                    app.status = Some(format!("created {} ({})", name, label));
                    app.mode = Mode::Menu;
                }
                Err(err) => {
                    app.status = Some(format!("failed: {}", err));
                    app.mode = Mode::Menu;
                }
            }
        }
        _ => {}
    }
    Ok(false)
}

fn handle_running_key(app: &mut AppState, code: KeyCode) -> io::Result<bool> {
    let Mode::Running(state) = &mut app.mode else {
        return Ok(false);
    };
    match code {
        KeyCode::Char('q') => {
            stop_child(state);
            app.mode = Mode::Menu;
            app.status = Some("server stopped".to_string());
        }
        KeyCode::Esc => {
            if state.done {
                app.mode = Mode::Menu;
                app.status = None;
            }
        }
        KeyCode::Enter => {
            if state.done {
                app.mode = Mode::Menu;
                app.status = None;
            }
        }
        _ => {}
    }
    Ok(false)
}

fn start_running() -> io::Result<RunningState> {
    let (log_tx, log_rx) = mpsc::channel::<String>();
    let mut child = Command::new("cargo")
        .arg("run")
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()?;

    if let Some(stdout) = child.stdout.take() {
        spawn_log_reader(stdout, log_tx.clone());
    }
    if let Some(stderr) = child.stderr.take() {
        spawn_log_reader(stderr, log_tx.clone());
    }

    Ok(RunningState {
        child: Some(child),
        log_rx,
        logs: Vec::new(),
        start_time: Instant::now(),
        status: "starting".to_string(),
        done: false,
    })
}

fn stop_child(state: &mut RunningState) {
    if let Some(mut child) = state.child.take() {
        let _ = child.kill();
        let _ = child.wait();
        state.status = "stopped".to_string();
        state.done = true;
    }
}

fn drain_logs(state: &mut RunningState) {
    while let Ok(line) = state.log_rx.try_recv() {
        state.logs.push(line);
        if state.logs.len() > 1000 {
            state.logs.drain(0..200);
        }
    }
    if !state.done {
        let elapsed = state.start_time.elapsed().as_secs();
        if elapsed > 0 {
            state.status = format!("running {}s", elapsed);
        }
    }
}

fn spawn_log_reader<R: io::Read + Send + 'static>(reader: R, tx: mpsc::Sender<String>) {
    thread::spawn(move || {
        let buf = BufReader::new(reader);
        for line in buf.lines().flatten() {
            let _ = tx.send(line);
        }
    });
}

fn create_project(name: &str, framework: FrameWork) -> io::Result<PathBuf> {
    let dir = PathBuf::from(name);
    if dir.exists() {
        return Err(io::Error::new(
            io::ErrorKind::AlreadyExists,
            "directory exists",
        ));
    }
    fs::create_dir_all(dir.join("src"))?;

    let package_name = sanitize_package_name(name);
    if package_name.is_empty() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            "invalid package name",
        ));
    }

    let cargo_toml = format!(
        "[package]\nname = \"{}\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\noker = \"0.2.0\"\n",
        package_name
    );
    fs::write(dir.join("Cargo.toml"), cargo_toml)?;

    let main_rs = r##"use oker::{App, HttpResponse, Request, RequestType};

fn main() {
    let mut app = App::new(Box::new(|_req: &Request| {
        HttpResponse::Html("<h1>hello world!</h1>".to_string())
    }));

    app.route(
        RequestType::GET,
        "/json".to_string(),
        Box::new(|_req: &Request| HttpResponse::Json(r#"{"msg":"world"}"#.to_string())),
    );

    app.route(
        RequestType::GET,
        "/xml".to_string(),
        Box::new(|_req: &Request| HttpResponse::Xml("<msg>world</msg>".to_string())),
    );

    // Serve built UI assets (run `npm run build` in ui/ first)
    app.serve_ui("ui/dist");

    app.start();
}
"##;
    fs::write(dir.join("src/main.rs"), main_rs)?;

    create_ui(dir.join("ui"), name, framework)?;

    Ok(dir)
}

fn sanitize_package_name(name: &str) -> String {
    let mut out = String::new();
    for ch in name.chars() {
        if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' {
            out.push(ch.to_ascii_lowercase());
        } else if ch.is_whitespace() {
            out.push('-');
        }
    }
    out.trim_matches('-').to_string()
}

fn parse_framework_name(input: &str) -> Option<FrameWork> {
    let value = input.trim().to_ascii_lowercase();
    match value.as_str() {
        "svelte" => Some(FrameWork::Svelte),
        "vue" => Some(FrameWork::Vue),
        "react" => Some(FrameWork::React),
        "astro" => Some(FrameWork::Astro),
        "lit" => Some(FrameWork::Lit),
        "quasar" => Some(FrameWork::Quasar),
        "alpine" => Some(FrameWork::Alpine),
        "vanilla" => Some(FrameWork::Vanilla),
        _ => None,
    }
}

fn create_ui(dir: PathBuf, project_name: &str, framework: FrameWork) -> io::Result<()> {
    let wasm_module = create_wasm_package(dir.join("wasm"), project_name)?;
    match framework {
        FrameWork::Astro => create_astro_ui(dir, project_name, &wasm_module),
        FrameWork::Quasar => create_quasar_ui(dir, project_name, &wasm_module),
        _ => create_vite_ui(dir, project_name, framework, &wasm_module),
    }
}

fn create_vite_ui(
    dir: PathBuf,
    project_name: &str,
    framework: FrameWork,
    wasm_module: &str,
) -> io::Result<()> {
    fs::create_dir_all(dir.join("src"))?;

    let (index_html, main_file, main_content, extra_files) = match framework {
        FrameWork::React => (
            r#"<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Oker React</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>
"#,
            "main.jsx",
            r##"import React from "react";
import ReactDOM from "react-dom/client";
import init, { add } from "../wasm/pkg/{{WASM_MODULE}}.js";

async function initWasm() {
  await init();
  const value = add(2, 3);
  const el = document.getElementById("wasm-result");
  if (el) el.textContent = value.toString();
}

initWasm();

function App() {
  return (
    <main style={{ fontFamily: "system-ui", padding: "2rem" }}>
      <h1>Hello world!</h1>
      <p>React + Oker</p>
      <p>WASM add(2,3) = <span id="wasm-result">...</span></p>
    </main>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
"##,
            Vec::new(),
        ),
        FrameWork::Vue => (
            r#"<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Oker Vue</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
"#,
            "main.js",
            r##"import { createApp } from "vue";
import init, { add } from "../wasm/pkg/{{WASM_MODULE}}.js";

createApp({
  data() {
    return { message: "Hello world!", wasm: "..." };
  },
  async mounted() {
    await init();
    this.wasm = add(2, 3).toString();
  },
  template: `<main style="font-family: system-ui; padding: 2rem;">
    <h1>{{ message }}</h1>
    <p>Vue + Oker</p>
    <p>WASM add(2,3) = {{ wasm }}</p>
  </main>`,
}).mount("#app");
"##,
            Vec::new(),
        ),
        FrameWork::Svelte => (
            r#"<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Oker Svelte</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
"#,
            "main.js",
            r##"import App from "./App.svelte";
import init, { add } from "../wasm/pkg/{{WASM_MODULE}}.js";

async function initWasm() {
  await init();
  const value = add(2, 3);
  const el = document.getElementById("wasm-result");
  if (el) el.textContent = value.toString();
}
initWasm();

const app = new App({
  target: document.getElementById("app"),
});

export default app;
"##,
            vec![("App.svelte", r#"<main style="font-family: system-ui; padding: 2rem;">
  <h1>Hello world!</h1>
  <p>Svelte + Oker</p>
  <p>WASM add(2,3) = <span id="wasm-result">...</span></p>
</main>
"#)],
        ),
        FrameWork::Lit => (
            r#"<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Oker Lit</title>
  </head>
  <body>
    <hello-lit></hello-lit>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
"#,
            "main.js",
            r##"import { LitElement, html, css } from "lit";
import init, { add } from "../wasm/pkg/{{WASM_MODULE}}.js";

class HelloLit extends LitElement {
  static styles = css`
    :host {
      display: block;
      font-family: system-ui;
      padding: 2rem;
    }
  `;

  async connectedCallback() {
    super.connectedCallback();
    await init();
    this.wasm = add(2, 3).toString();
    this.requestUpdate();
  }

  render() {
    return html`<h1>Hello world!</h1>
      <p>Lit + Oker</p>
      <p>WASM add(2,3) = ${this.wasm ?? "..."}</p>`;
  }
}

customElements.define("hello-lit", HelloLit);
"##,
            Vec::new(),
        ),
        FrameWork::Alpine => (
            r#"<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Oker Alpine</title>
  </head>
  <body>
    <main x-data="{ message: 'Hello world!', wasm: '...' }" style="font-family: system-ui; padding: 2rem;">
      <h1 x-text="message"></h1>
      <p>Alpine + Oker</p>
      <p>WASM add(2,3) = <span x-text="wasm"></span></p>
    </main>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
"#,
            "main.js",
            r##"import Alpine from "alpinejs";
import init, { add } from "../wasm/pkg/{{WASM_MODULE}}.js";

window.Alpine = Alpine;
Alpine.start();

async function initWasm() {
  await init();
  const value = add(2, 3).toString();
  const root = document.querySelector("[x-data]");
  if (root && root.__x) {
    root.__x.$data.wasm = value;
  }
}
initWasm();
"##,
            Vec::new(),
        ),
        _ => (
            r#"<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Oker Vanilla</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
"#,
            "main.js",
            r##"import init, { add } from "../wasm/pkg/{{WASM_MODULE}}.js";

document.querySelector("#app").innerHTML = `
  <main style="font-family: system-ui; padding: 2rem;">
    <h1>Hello world!</h1>
    <p>Vanilla + Oker</p>
    <p>WASM add(2,3) = <span id="wasm-result">...</span></p>
  </main>
`;

async function initWasm() {
  await init();
  const value = add(2, 3);
  const el = document.getElementById("wasm-result");
  if (el) el.textContent = value.toString();
}

initWasm();
"##,
            Vec::new(),
        ),
    };

    let package_json = vite_package_json(project_name, framework);
    fs::write(dir.join("package.json"), package_json)?;
    fs::write(dir.join("index.html"), index_html)?;
    let main_content = main_content.replace("{{WASM_MODULE}}", wasm_module);
    fs::write(dir.join("src").join(main_file), main_content)?;
    for (name, content) in extra_files {
        fs::write(dir.join("src").join(name), content)?;
    }

    if let Some(config) = vite_config(framework) {
        fs::write(dir.join("vite.config.js"), config)?;
    }

    let readme = r#"# UI (Vite)

## WASM setup

1. Install wasm-pack:
   - `cargo install wasm-pack`
2. Run the dev server:
   - `npm run dev`
"#;
    fs::write(dir.join("README.md"), readme)?;

    Ok(())
}

fn vite_package_json(project_name: &str, framework: FrameWork) -> String {
    let name = format!("{}-ui", sanitize_package_name(project_name));
    let (deps, dev_deps) = match framework {
        FrameWork::React => (
            r#""react": "^18.2.0",
    "react-dom": "^18.2.0""#,
            r#""@vitejs/plugin-react": "^4.2.0",
    "vite": "^5.0.0""#,
        ),
        FrameWork::Vue => (
            r#""vue": "^3.4.0""#,
            r#""@vitejs/plugin-vue": "^5.0.0",
    "vite": "^5.0.0""#,
        ),
        FrameWork::Svelte => (
            r#""svelte": "^4.2.0""#,
            r#""@sveltejs/vite-plugin-svelte": "^3.0.0",
    "vite": "^5.0.0""#,
        ),
        FrameWork::Lit => (
            r#""lit": "^3.1.0""#,
            r#""vite": "^5.0.0""#,
        ),
        FrameWork::Alpine => (
            r#""alpinejs": "^3.13.0""#,
            r#""vite": "^5.0.0""#,
        ),
        _ => ("", r#""vite": "^5.0.0""#),
    };

    let deps_block = if deps.is_empty() {
        "\"dependencies\": {}".to_string()
    } else {
        format!("\"dependencies\": {{\n    {}\n  }}", deps)
    };

    format!(
        "{{\n  \"name\": \"{}\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {{\n    \"wasm\": \"wasm-pack build wasm --target web --out-dir pkg\",\n    \"dev\": \"npm run wasm && vite\",\n    \"build\": \"npm run wasm && vite build\",\n    \"preview\": \"vite preview\"\n  }},\n  {},\n  \"devDependencies\": {{\n    {}\n  }}\n}}\n",
        name, deps_block, dev_deps
    )
}

fn vite_config(framework: FrameWork) -> Option<String> {
    match framework {
        FrameWork::React => Some(
            r#"import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
});
"#
            .to_string(),
        ),
        FrameWork::Vue => Some(
            r#"import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  plugins: [vue()],
});
"#
            .to_string(),
        ),
        FrameWork::Svelte => Some(
            r#"import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";

export default defineConfig({
  plugins: [svelte()],
});
"#
            .to_string(),
        ),
        _ => None,
    }
}

fn create_astro_ui(dir: PathBuf, project_name: &str, wasm_module: &str) -> io::Result<()> {
    fs::create_dir_all(dir.join("src").join("pages"))?;
    let name = format!("{}-ui", sanitize_package_name(project_name));
    let package_json = format!(
        "{{\n  \"name\": \"{}\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {{\n    \"wasm\": \"wasm-pack build wasm --target web --out-dir pkg\",\n    \"dev\": \"npm run wasm && astro dev\",\n    \"build\": \"npm run wasm && astro build\",\n    \"preview\": \"astro preview\"\n  }},\n  \"dependencies\": {{\n    \"astro\": \"^4.0.0\"\n  }}\n}}\n",
        name
    );
    let astro_config = r#"import { defineConfig } from "astro/config";

export default defineConfig({});
"#;
    let index = format!(
        r#"---
const message = "Hello world!";
---
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Oker Astro</title>
  </head>
  <body>
    <main style="font-family: system-ui; padding: 2rem;">
      <h1>{{message}}</h1>
      <p>Astro + Oker</p>
      <p>WASM add(2,3) = <span id="wasm-result">...</span></p>
    </main>
    <script type="module">
      import init, {{ add }} from "../wasm/pkg/{module}.js";
      await init();
      const value = add(2, 3);
      const el = document.getElementById("wasm-result");
      if (el) el.textContent = value.toString();
    </script>
  </body>
</html>
"#,
        module = wasm_module
    );
    fs::write(dir.join("package.json"), package_json)?;
    fs::write(dir.join("astro.config.mjs"), astro_config)?;
    fs::write(dir.join("src").join("pages").join("index.astro"), index)?;
    let readme = r#"# UI (Astro)

## WASM setup

1. Install wasm-pack:
   - `cargo install wasm-pack`
2. Run the dev server:
   - `npm run dev`
"#;
    fs::write(dir.join("README.md"), readme)?;
    Ok(())
}

fn create_quasar_ui(dir: PathBuf, _project_name: &str, wasm_module: &str) -> io::Result<()> {
    fs::create_dir_all(&dir)?;
    let readme = format!(
        "# Quasar UI\n\nThis template is a placeholder for Quasar.\n\n## Next steps\n\n1. Enter the directory:\n   - `cd {}`\n2. Create a Quasar app:\n   - `npm create quasar@latest .`\n3. Start dev server:\n   - `npm install`\n   - `npm run dev`\n\n## WASM\n\nThe wasm crate was generated at `ui/wasm`.\n\n1. Install wasm-pack:\n   - `cargo install wasm-pack`\n2. Build wasm:\n   - `cd wasm && wasm-pack build --target web --out-dir pkg`\n\nImport in Quasar:\n- `import init, {{ add }} from \"../wasm/pkg/{}.js\";`\n\nSelected framework: quasar\n",
        dir.display(),
        wasm_module
    );
    fs::write(dir.join("README.md"), readme)?;
    Ok(())
}

fn create_wasm_package(dir: PathBuf, project_name: &str) -> io::Result<String> {
    fs::create_dir_all(dir.join("src"))?;
    let base = sanitize_crate_name(project_name);
    let crate_name = format!("{}_wasm", base);
    let cargo_toml = format!(
        "[package]\nname = \"{}\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[lib]\ncrate-type = [\"cdylib\"]\n\n[dependencies]\nwasm-bindgen = \"0.2\"\noker = \"0.1.0\"\n",
        crate_name
    );
    let lib_rs = r#"use oker::oker_js;

oker_js! {
    fn add(a: i32, b: i32) -> i32 {
        a + b
    }
}

oker_js! {
    fn greet(name: &str) -> String {
        format!("Hello, {}!", name)
    }
}
"#;
    fs::write(dir.join("Cargo.toml"), cargo_toml)?;
    fs::write(dir.join("src").join("lib.rs"), lib_rs)?;
    Ok(crate_name)
}

fn sanitize_crate_name(name: &str) -> String {
    let mut out = String::new();
    for ch in name.chars() {
        if ch.is_ascii_alphanumeric() || ch == '_' {
            out.push(ch.to_ascii_lowercase());
        } else if ch == '-' || ch.is_whitespace() {
            out.push('_');
        }
    }
    let trimmed = out.trim_matches('_').to_string();
    let mut final_name = if trimmed.is_empty() {
        "oker".to_string()
    } else {
        trimmed
    };
    let first = final_name.chars().next().unwrap_or('o');
    if !first.is_ascii_alphabetic() && first != '_' {
        final_name = format!("oker_{}", final_name);
    }
    final_name
}
