/**
 * Shared launcher for `flutter run` invocations.
 *
 * Both `kasy run` and `kasy reset` (reinstall step) call this so the spinner,
 * stage detection, elapsed timer, hot-reload stdin pass-through and SIGINT
 * forwarding stay consistent in one place.
 *
 * Flow:
 *  - Spawn flutter with piped stdio.
 *  - Show a Clack spinner with elapsed time, ticking every second.
 *  - As Flutter logs scroll, update the spinner message when we see a known
 *    stage marker (Gradle, Xcode, install, sync, etc.).
 *  - When Flutter signals "ready" (DAP key commands), flush buffered output,
 *    drop into stdio pass-through and pipe stdin so hot reload works.
 *  - On early exit, replay the buffer so the user can see the error.
 */

'use strict';

const { spawn } = require('node:child_process');
const kleur = require('kleur');
const ui = require('./ui');

// Markers that tell us the initial build is done and the app is running.
const FLUTTER_READY_RE = /Flutter run key commands\.|is listening on|VM Service|Dart VM service|To hot reload|Hot restart/i;

/**
 * Map a chunk of Flutter output to a user-friendly stage label, or null
 * if the chunk doesn't carry any of the known markers.
 */
function detectStage(text, t) {
  if (/Running Gradle task 'assembleDebug'/.test(text)) {
    return t('run.stage.gradleFirstTime');
  }
  if (/Running Gradle task/.test(text)) {
    return t('run.stage.gradle');
  }
  if (/Running Xcode build/.test(text)) {
    return t('run.stage.xcode');
  }
  if (/Running pod install/i.test(text)) {
    return t('run.stage.pods');
  }
  if (/Installing build\/.+\.(apk|ipa|app)/i.test(text)) {
    return t('run.stage.installing');
  }
  if (/Syncing files to device/i.test(text)) {
    return t('run.stage.syncing');
  }
  if (/BUILD SUCCESSFUL/.test(text)) {
    return t('run.stage.buildSuccess');
  }
  return null;
}

function formatElapsed(startTime) {
  const elapsed = Math.floor((Date.now() - startTime) / 1000);
  if (elapsed < 60) return `${elapsed}s`;
  const m = Math.floor(elapsed / 60);
  const s = elapsed % 60;
  return `${m}m ${s}s`;
}

function render(stageMessage, startTime) {
  return `${stageMessage}  ${kleur.dim(`[${formatElapsed(startTime)}]`)}`;
}

/**
 * Spawn `flutter` with the given args inside `projectDir`, surfacing progress
 * through a Clack spinner. Resolves on success, rejects on non-zero exit.
 */
function spawnFlutterWithSpinner(args, projectDir, t) {
  return new Promise((resolve, reject) => {
    const proc = spawn('flutter', args, {
      cwd: projectDir,
      stdio: ['pipe', 'pipe', 'pipe'],
    });

    const spinner = ui.spinner();
    const startTime = Date.now();
    let stageMessage = t('run.spinner.building');
    let ready = false;
    const buffer = [];

    spinner.start(render(stageMessage, startTime));

    // Tick the elapsed time every second while still building. Once ready,
    // the spinner is gone and we stop ticking.
    const tick = setInterval(() => {
      if (!ready) spinner.message(render(stageMessage, startTime));
    }, 1000);

    const flushAndSwitch = () => {
      if (ready) return;
      ready = true;
      clearInterval(tick);
      const total = formatElapsed(startTime);
      spinner.stop(`${t('run.spinner.ready')}  ${kleur.dim(`[${total}]`)}`);
      for (const chunk of buffer) process.stdout.write(chunk);
      buffer.length = 0;
      // Pipe stdin so the user can type r / R / q to control Flutter.
      if (process.stdin.isTTY) {
        try { process.stdin.setRawMode(true); } catch (_) {}
        process.stdin.resume();
        process.stdin.pipe(proc.stdin);
      }
    };

    const handleStdout = (chunk) => {
      const text = chunk.toString();
      if (!ready) {
        buffer.push(chunk);
        const nextStage = detectStage(text, t);
        if (nextStage && nextStage !== stageMessage) {
          stageMessage = nextStage;
          spinner.message(render(stageMessage, startTime));
        }
        if (FLUTTER_READY_RE.test(text)) flushAndSwitch();
      } else {
        process.stdout.write(chunk);
      }
    };

    const handleStderr = (chunk) => {
      if (!ready) {
        buffer.push(chunk);
        if (FLUTTER_READY_RE.test(chunk.toString())) flushAndSwitch();
      } else {
        process.stderr.write(chunk);
      }
    };

    proc.stdout.on('data', handleStdout);
    proc.stderr.on('data', handleStderr);

    const sigintHandler = () => { try { proc.kill('SIGINT'); } catch (_) {} };
    process.on('SIGINT', sigintHandler);

    const cleanup = () => {
      clearInterval(tick);
      process.off('SIGINT', sigintHandler);
      if (process.stdin.isTTY) {
        try { process.stdin.setRawMode(false); } catch (_) {}
        process.stdin.unpipe(proc.stdin);
        process.stdin.pause();
      }
    };

    proc.on('close', (code) => {
      cleanup();
      if (!ready) {
        spinner.stop(t('run.spinner.failed'), 2);
        for (const chunk of buffer) process.stdout.write(chunk);
      }
      if (code === 0) resolve();
      else reject(new Error(`flutter exited with code ${code}`));
    });

    proc.on('error', (err) => {
      cleanup();
      if (!ready) spinner.stop(t('run.spinner.failed'), 2);
      reject(err);
    });
  });
}

module.exports = {
  spawnFlutterWithSpinner,
  // Exported for tests and reuse in non-flutter spawn helpers.
  detectStage,
  formatElapsed,
  FLUTTER_READY_RE,
};
