import express from 'express';
import compression from 'compression';
import { exec, spawn } from 'child_process';
import { existsSync, mkdirSync, cpSync, rmSync, readdirSync, readFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';

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

const PORT = process.env.PORT || 3000;
const GIT_REPO_URL = process.env.GIT_REPO_URL;
const GIT_BRANCH = process.env.GIT_BRANCH || 'main';
const VITE_CONVEX_URL = process.env.VITE_CONVEX_URL;
const VITE_LIVEKIT_URL = process.env.VITE_LIVEKIT_URL;
const RELEASES_DIR = '/app/releases';
const LOADING_HTML = readFileSync(join(__dirname, 'loading.html'), 'utf-8');

let currentDistPath = null;
let currentCommitHash = null;
let building = false;
let lastBuildStatus = { status: 'idle', timestamp: null, error: null };

// --- Helpers ---

function log(msg) {
  console.log(`[${new Date().toISOString()}] ${msg}`);
}

function findLatestRelease() {
  if (!existsSync(RELEASES_DIR)) return null;
  const entries = readdirSync(RELEASES_DIR).sort();
  if (entries.length === 0) return null;
  const latest = join(RELEASES_DIR, entries[entries.length - 1]);
  if (existsSync(join(latest, 'index.html'))) return latest;
  return null;
}

async function run(cmd, opts = {}) {
  log(`> ${cmd}`);
  return new Promise((resolve, reject) => {
    const child = spawn('sh', ['-c', cmd], {
      stdio: 'inherit',
      timeout: 600_000,
      ...opts,
    });
    child.on('close', (code) => {
      if (code !== 0) reject(new Error(`Command failed with exit code ${code}: ${cmd}`));
      else resolve();
    });
    child.on('error', reject);
  });
}

async function triggerBuild(webhookCommit) {
  if (building) return;

  // Fast path: skip clone entirely if webhook provides the commit and it matches
  if (webhookCommit && webhookCommit === currentCommitHash) {
    log(`Commit ${webhookCommit} is already deployed, skipping build (webhook fast path).`);
    return;
  }

  building = true;
  lastBuildStatus = { status: 'building', timestamp: Date.now(), error: null };

  const timestamp = Date.now().toString();
  const buildDir = `/tmp/build-${timestamp}`;
  const releaseDir = join(RELEASES_DIR, timestamp);

  try {
    log('Starting build...');

    // Clone
    await run(`git clone --depth 1 --branch ${GIT_BRANCH} ${GIT_REPO_URL} ${buildDir}`);

    // Read the cloned commit hash
    const { stdout: hashOut } = await new Promise((resolve, reject) => {
      exec('git rev-parse HEAD', { cwd: buildDir }, (err, stdout, stderr) => {
        if (err) reject(err);
        else resolve({ stdout, stderr });
      });
    });
    const clonedHash = hashOut.trim();
    log(`Cloned commit: ${clonedHash}`);

    // Skip if this commit is already deployed
    if (clonedHash === currentCommitHash) {
      log(`Commit ${clonedHash} is already deployed, skipping build.`);
      lastBuildStatus = { status: 'skipped', timestamp: Date.now(), error: null };
      return;
    }

    // Install deps (web workspaces only)
    await run(
      'npm ci --workspace=apps/web --workspace=packages/shared --workspace=packages/platform-web --include-workspace-root',
      { cwd: buildDir }
    );

    // Build
    const env = { ...process.env, VITE_CONVEX_URL, VITE_LIVEKIT_URL };
    await run('npm run build:web', { cwd: buildDir, env });

    // Copy dist to release dir
    const distDir = join(buildDir, 'apps', 'web', 'dist');
    if (!existsSync(distDir)) throw new Error('Build did not produce apps/web/dist/');
    mkdirSync(releaseDir, { recursive: true });
    cpSync(distDir, releaseDir, { recursive: true });

    // Swap — instant, zero downtime
    const oldDist = currentDistPath;
    currentDistPath = releaseDir;
    currentCommitHash = clonedHash;
    log(`Swapped to new release: ${releaseDir} (commit ${clonedHash})`);

    // Clean up old release
    if (oldDist && existsSync(oldDist) && oldDist !== releaseDir) {
      rmSync(oldDist, { recursive: true, force: true });
      log(`Deleted old release: ${oldDist}`);
    }

    lastBuildStatus = { status: 'success', timestamp: Date.now(), error: null };
    log('Build complete.');
  } catch (err) {
    lastBuildStatus = { status: 'failed', timestamp: Date.now(), error: err.message };
    log(`Build failed: ${err.message}`);
    // Clean up failed release dir if it was created
    if (existsSync(releaseDir)) rmSync(releaseDir, { recursive: true, force: true });
  } finally {
    // Always clean up temp build dir
    if (existsSync(buildDir)) rmSync(buildDir, { recursive: true, force: true });
    building = false;
  }
}

// --- Express App ---

const app = express();

app.use(compression());

// Webhook endpoint
app.post('/api/webhook', express.json(), (req, res) => {
  if (building) return res.status(409).json({ error: 'Build already in progress' });

  // Extract commit SHA from Gitea/GitHub push webhook payload
  const webhookCommit = req.body?.after || null;

  triggerBuild(webhookCommit);
  res.json({ message: 'Build triggered' });
});

// Status endpoint
app.get('/api/status', (_req, res) => {
  res.json({ building, commitHash: currentCommitHash, ...lastBuildStatus });
});

// Static file serving + SPA fallback
app.use((req, res, next) => {
  // While no release exists, serve loading page
  if (!currentDistPath) return res.status(503).type('html').send(LOADING_HTML);

  // Serve static files from the current release
  express.static(currentDistPath, {
    maxAge: req.path.startsWith('/assets/') ? '1y' : 0,
    immutable: req.path.startsWith('/assets/'),
    index: false, // We handle index.html ourselves for SPA fallback
  })(req, res, () => {
    // SPA fallback: if no static file matched, serve index.html
    res.sendFile(join(currentDistPath, 'index.html'));
  });
});

// --- Start ---

mkdirSync(RELEASES_DIR, { recursive: true });

// Check for existing release
currentDistPath = findLatestRelease();
if (currentDistPath) {
  log(`Found existing release: ${currentDistPath}`);
} else {
  log('No existing release found, will build on startup.');
}

app.listen(PORT, () => {
  log(`Server listening on port ${PORT}`);

  // Auto-build on first start if no release exists
  if (!currentDistPath && GIT_REPO_URL) {
    triggerBuild();
  }
});
