#!/usr/bin/env node
import fs from 'fs';
import path from 'path';
import os from 'os';
import { fileURLToPath } from 'url';
import { pipeline } from 'stream/promises';
import fetch from 'node-fetch';
import tar from 'tar';
import unzipper from 'unzipper';
import ProgressBar from 'progress';
import chalk from 'chalk';
import { resolveTarget, artifactName } from './platform.js';
import crypto from 'crypto';
import { HttpsProxyAgent } from 'https-proxy-agent';

// Allow CI to skip downloading the binary
if (process.env.MARAVILLA_CLI_SKIP_INSTALL === '1' || process.env.CI) {
  console.log('✿ Skipping Maravilla CLI binary install (CI or skip flag set)');
  process.exit(0);
}

import { spawn } from 'child_process';

// Detect if output is being suppressed (npm 7+ behavior)
// Write to stderr as a fallback since it's more likely to be shown
const isOutputSuppressed = !process.stdout.isTTY && !process.env.npm_config_foreground_scripts;
if (isOutputSuppressed) {
  console.error('✿ Installing Maravilla CLI binary...');
  console.error('✿ To see detailed progress, run: npm install --foreground-scripts');
}

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const BIN_NAME = 'maravilla';
const REPO = process.env.MARAVILLA_CLI_REPO || 'maravilla-labs/maravilla-cli';
const VERSION = process.env.MARAVILLA_VERSION || 'latest'; // cli-vX.Y.Z or 'latest'
const BIN_DIR = path.join(__dirname, '..', 'bin');
const EXECUTABLE = process.platform === 'win32' ? `${BIN_NAME}.exe` : BIN_NAME;
const INSTALL_PATH = path.join(BIN_DIR, EXECUTABLE);
const GH_TOKEN = process.env.MARAVILLA_GH_TOKEN || process.env.GITHUB_TOKEN || '';
const PROXY = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || '';
const HTTP_TIMEOUT_MS = Number(process.env.MARAVILLA_HTTP_TIMEOUT_MS || 15000);
const HTTP_RETRIES = Math.max(1, Number(process.env.MARAVILLA_HTTP_RETRIES || 3));

function flower() {
  return chalk.magenta('✿');
}

function log(msg) {
  console.log(`${flower()} ${msg}`);
}

function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }

async function abortableFetch(url, opts = {}, timeoutMs = 30000) {
  const controller = new AbortController();
  const t = setTimeout(() => controller.abort(), timeoutMs);
  try {
  return await fetch(url, { ...opts, signal: controller.signal });
  } finally {
    clearTimeout(t);
  }
}

async function withRetries(taskFn, tries = HTTP_RETRIES, baseDelay = 1000) {
  let attempt = 0;
  let lastErr;
  while (attempt < tries) {
    try {
      return await taskFn();
    } catch (e) {
      lastErr = e;
      attempt++;
      if (attempt >= tries) break;
      const delay = baseDelay * Math.pow(2, attempt - 1);
      console.log(chalk.yellow(`${flower()} retrying in ${Math.round(delay/100)/10}s...`));
      await sleep(delay);
    }
  }
  throw lastErr;
}

function makeFetchOpts() {
  const headers = { 'User-Agent': 'maravilla-cli-installer' };
  if (GH_TOKEN) headers['Authorization'] = `Bearer ${GH_TOKEN}`;
  const opts = { headers };
  if (PROXY) {
    try { opts.agent = new HttpsProxyAgent(PROXY); } catch {}
  }
  return opts;
}

async function getReleaseTag() {
  if (VERSION && VERSION !== 'latest') return VERSION;
  const api = `https://api.github.com/repos/${REPO}/releases/latest`;
  const opts = makeFetchOpts();
  try {
    console.log(chalk.gray(`${flower()} fetching latest release tag (${Math.round(HTTP_TIMEOUT_MS/100)/10}s timeout, ${HTTP_RETRIES} retries)`));
    const res = await withRetries(() => abortableFetch(api, opts, HTTP_TIMEOUT_MS));
    if (!res.ok) throw new Error(`Failed to fetch latest release: ${res.status} ${res.statusText}`);
    const json = await res.json();
    if (!json.tag_name) throw new Error('No tag_name in API response');
    return json.tag_name;
  } catch (e) {
    console.log(chalk.yellow(`${flower()} API lookup failed (${e.message}). Falling back to HTML redirect...`));
    // Fallback: use HTML redirect to /releases/tag/<tag>
    const html = `https://github.com/${REPO}/releases/latest`;
    const res = await abortableFetch(html, { ...opts, redirect: 'manual' }, Math.min(HTTP_TIMEOUT_MS, 10000));
    const loc = res.headers.get('location') || '';
    const m = loc.match(/\/releases\/tag\/([^/]+)$/);
    if (m) return m[1];
    throw new Error('Could not resolve latest tag via fallback');
  }
}

async function downloadWithProgress(url, dest) {
  const res = await withRetries(() => abortableFetch(url, {}, 60000));
  if (!res.ok) throw new Error(`Failed to download ${url}: ${res.status} ${res.statusText}`);
  const total = Number(res.headers.get('content-length')) || 0;
  const bar = total ? new ProgressBar(`${flower()} downloading [:bar] :percent :etas`, {
    complete: '=', incomplete: ' ', width: 24, total
  }) : null;

  await pipeline(
    res.body,
    new (class extends fs.WriteStream {
      constructor(dest) { super(dest); }
      write(chunk, enc, cb) {
        if (bar) bar.tick(chunk.length);
        else process.stdout.write('.');
        super.write(chunk, enc, cb);
      }
    })(dest)
  );
  if (!bar) process.stdout.write('\n');
}

async function extractArchive(archivePath, targetDir, ext) {
  if (ext === 'tar.gz') {
    await tar.x({ file: archivePath, cwd: targetDir });
  } else if (ext === 'zip') {
    await fs.createReadStream(archivePath).pipe(unzipper.Extract({ path: targetDir })).promise();
  } else {
    throw new Error(`Unknown archive type: ${ext}`);
  }
}

async function currentVersion() {
  return new Promise((resolve) => {
    const child = spawn(INSTALL_PATH, ['--version'], { stdio: ['ignore', 'pipe', 'ignore'] });
    let out = '';
    child.stdout.on('data', (d) => (out += d.toString()));
    child.on('error', () => resolve(null));
    child.on('exit', (code) => {
      if (code === 0) {
        const v = (out || '').trim();
        const m = v.match(/(\d+\.\d+\.\d+)/);
        resolve(m ? m[1] : null);
      } else {
        resolve(null);
      }
    });
  });
}

function cmpSemver(a, b) {
  if (!a || !b) return 1;
  const pa = a.split('.').map(Number);
  const pb = b.split('.').map(Number);
  for (let i = 0; i < 3; i++) {
    if (pa[i] > pb[i]) return 1;
    if (pa[i] < pb[i]) return -1;
  }
  return 0;
}

async function install() {
  const { target, ext } = resolveTarget();

  // Detect current installed version first to enable true offline fast-path
  let cur = null;
  try { cur = await currentVersion(); } catch {}

  if (process.env.MARAVILLA_OFFLINE === '1') {
    if (cur) {
      log(chalk.yellow(`Offline mode: keeping existing v${cur}`));
      return;
    }
    throw new Error('Offline mode: no existing binary; set MARAVILLA_VERSION to a specific tag or retry online');
  }

  log(`Resolving latest release tag from ${chalk.cyan(REPO)}`);
  const tag = await getReleaseTag();
  const art = artifactName(BIN_NAME, tag, target);
  const assetFile = `${art}.${ext}`;
  const url = `https://github.com/${REPO}/releases/download/${tag}/${assetFile}`;
  const sumsUrl = `https://github.com/${REPO}/releases/download/${tag}/SHA256SUMS`;

  await fs.promises.mkdir(BIN_DIR, { recursive: true });
  const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'maravilla-cli-'));
  const archive = path.join(tmpDir, assetFile);

  // Decide whether to download based on current vs requested version
  const force = process.env.MARAVILLA_FORCE_INSTALL === '1';
  let requested = null;
  // Handle both "cli-v0.1.9" and "v0.1.9" formats
  const m = tag.match(/v(\d+\.\d+\.\d+)/);
  if (m) requested = m[1];

  if (!force) {
    if (cur && requested && cmpSemver(cur, requested) >= 0) {
      log(chalk.green(`Already up to date (v${cur})`));
      return;
    }
  }

  log(chalk.bold(`Installing ${BIN_NAME} ${chalk.cyan(tag)} for ${chalk.cyan(target)}`));
  log(`Downloading from ${url}`);
  await downloadWithProgress(url, archive);

  // Verify checksum if SHA256SUMS is available
  try {
    log('Verifying checksum');
    const sumsRes = await abortableFetch(sumsUrl, makeFetchOpts(), 15000);
    if (sumsRes.ok) {
      const sumsText = await sumsRes.text();
      const expected = sumsText
        .split(/\r?\n/)
        .map((l) => l.trim())
        .filter(Boolean)
        .map((l) => l.split(/\s+/)) // [hash, filename]
        .find(([, fname]) => fname === assetFile);
      if (!expected) throw new Error('Checksum entry not found for asset');
      const [expectedHash] = expected;
      const hash = await sha256File(archive);
      if (hash !== expectedHash) {
        throw new Error(`Checksum mismatch for ${assetFile}: expected ${expectedHash}, got ${hash}`);
      }
      log(chalk.green('Checksum OK'));
    } else {
      log(chalk.yellow('SHA256SUMS not found; skipping verification'));
    }
  } catch (e) {
    console.error(chalk.red(`${flower()} checksum verification failed: ${e.message}`));
    throw e;
  }

  log('Extracting archive');
  await extractArchive(archive, tmpDir, ext);

  // Find the actual binary - it might be directly in tmpDir or in a subdirectory
  const innerDir = path.join(tmpDir, art);
  let srcPath = path.join(innerDir, EXECUTABLE);
  
  // Check if binary exists in expected location
  if (!fs.existsSync(srcPath)) {
    // Try direct path in tmpDir
    const directPath = path.join(tmpDir, EXECUTABLE);
    if (fs.existsSync(directPath)) {
      srcPath = directPath;
      log(chalk.yellow('Binary found in root of archive'));
    } else {
      // List directory contents to debug
      const files = await fs.promises.readdir(tmpDir, { recursive: true });
      console.error('Archive contents:', files);
      throw new Error(`Binary not found at expected paths. Looking for: ${EXECUTABLE}`);
    }
  }
  
  // Remove existing binary first to ensure clean replacement
  try {
    await fs.promises.unlink(INSTALL_PATH);
    log('Removed existing binary');
  } catch (e) {
    // File might not exist, that's okay
  }
  
  // Copy new binary
  await fs.promises.copyFile(srcPath, INSTALL_PATH);
  await fs.promises.chmod(INSTALL_PATH, 0o755);
  
  // Verify the file was actually copied
  const stats = await fs.promises.stat(INSTALL_PATH);
  log(chalk.green(`Installed to ${INSTALL_PATH} (${stats.size} bytes)`));

  // Self-check: run --version
  // Use absolute path and clear environment to avoid caching issues
  await new Promise((resolve, reject) => {
    log('Verifying install by running --version');
    const child = spawn(INSTALL_PATH, ['--version'], { 
      stdio: ['ignore', 'pipe', 'pipe'],
      env: { ...process.env, NODE_DISABLE_COLORS: '1' },
      shell: false
    });
    let out = '';
    child.stdout.on('data', (d) => (out += d.toString()))
    child.on('error', reject);
    child.on('exit', (code) => {
      if (code === 0) {
        const version = out.trim();
        log(chalk.green(`Version: ${version}`));
        
        // Extra check: if we expected a specific version, verify it
        if (requested && !version.includes(requested)) {
          log(chalk.yellow(`Warning: Expected version ${requested} but got ${version}`));
          log(chalk.yellow('The binary might be cached. Try running the command directly.'));
        }
        resolve();
      } else {
        reject(new Error(`Version check failed with code ${code}`));
      }
    });
  });
}

async function sha256File(filePath) {
  return new Promise((resolve, reject) => {
    const hash = crypto.createHash('sha256');
    const rs = fs.createReadStream(filePath);
    rs.on('error', reject);
    rs.on('data', (chunk) => hash.update(chunk));
    rs.on('end', () => resolve(hash.digest('hex')));
  });
}

install()
  .then(() => {
    if (isOutputSuppressed) {
      console.error('✿ Maravilla CLI binary installed successfully!');
    }
  })
  .catch(err => {
    console.error(chalk.red(`${flower()} install failed: ${err.message}`));
    process.exitCode = 1;
  });
