// app/api/install/run/route.ts
import { NextRequest, NextResponse } from "next/server";
import { PrismaClient } from "@prisma/client";
import { execFile } from "child_process";
import { promisify } from "util";
import fs from "fs";
import path from "path";
import { isInstalled } from "@/lib/install-state";
import { validateDbConnectionInput, buildMysqlUrl } from "@/lib/db-connection";
import { validateUsername } from "@/lib/validation";
import { hashPassword, generateSecureToken } from "@/lib/crypto";
import { updateEnvFile } from "@/lib/env-file";

const execFileAsync = promisify(execFile);
const LOCK_PATH = path.join(process.cwd(), ".install.lock");

interface InstallRequestBody {
  db: {
    host: string;
    port: string;
    database: string;
    user: string;
    password: string;
  };
  admin: {
    username: string;
    password: string;
  };
  mail?: {
    webhookSigningKey?: string;
    mxHost?: string;
    mxHostFallback?: string;
  };
}

function acquireLock(): boolean {
  try {
    // "wx" fails atomically if the file already exists — this is the
    // actual mutex, not just a courtesy check-then-write.
    fs.writeFileSync(LOCK_PATH, String(process.pid), { flag: "wx" });
    return true;
  } catch {
    return false;
  }
}

function releaseLock(): void {
  try {
    fs.unlinkSync(LOCK_PATH);
  } catch {
    /* already gone, fine */
  }
}

export async function POST(req: NextRequest) {
  // Defense in depth: middleware already blocks this once installed, but
  // never trust that alone for a route this sensitive.
  if (isInstalled()) {
    return NextResponse.json({ error: "Already installed" }, { status: 409 });
  }

  if (!acquireLock()) {
    return NextResponse.json(
      {
        error:
          "An installation is already in progress. Wait for it to finish, or check server logs if it appears stuck.",
      },
      { status: 409 }
    );
  }

  try {
    const body: InstallRequestBody | null = await req.json().catch(() => null);
    if (!body?.db || !body?.admin) {
      return NextResponse.json({ error: "Missing db or admin fields" }, { status: 400 });
    }

    const dbValidation = validateDbConnectionInput(body.db);
    if (!dbValidation.valid) {
      return NextResponse.json({ error: dbValidation.error }, { status: 422 });
    }

    const usernameValidation = validateUsername(body.admin.username);
    if (!usernameValidation.valid) {
      return NextResponse.json({ error: usernameValidation.error }, { status: 422 });
    }

    if (!body.admin.password || body.admin.password.length < 8) {
      return NextResponse.json(
        { error: "Admin password must be at least 8 characters" },
        { status: 422 }
      );
    }

    const databaseUrl = buildMysqlUrl(body.db);

    // --- 1. Confirm the DB is actually reachable before writing anything ---
    const probeClient = new PrismaClient({ datasourceUrl: databaseUrl });
    try {
      await probeClient.$queryRaw`SELECT 1`;
    } catch (err) {
      const message = err instanceof Error ? err.message.split("\n")[0] : "Connection failed";
      return NextResponse.json(
        { error: `Could not connect to database: ${message}` },
        { status: 422 }
      );
    } finally {
      await probeClient.$disconnect().catch(() => {});
    }

    // --- 2. Push the schema (creates tables) --------------------------------
    try {
      await execFileAsync(
        "npx",
        [
          "prisma",
          "db",
          "push",
          "--accept-data-loss",
          "--skip-generate",
          "--schema=prisma/schema.prisma",
        ],
        {
          cwd: process.cwd(),
          env: { ...process.env, DATABASE_URL: databaseUrl },
          timeout: 60_000,
        }
      );
    } catch (err) {
      const message =
        err instanceof Error ? err.message.split("\n").slice(0, 5).join(" ") : "Schema push failed";
      return NextResponse.json(
        { error: `Failed to set up database tables: ${message}` },
        { status: 500 }
      );
    }

    // --- 3. Create the admin account (idempotent — skip if one exists) -----
    const appClient = new PrismaClient({ datasourceUrl: databaseUrl });
    let adminCreated = false;
    try {
      const existingCount = await appClient.user.count();
      if (existingCount === 0) {
        const passwordHash = await hashPassword(body.admin.password);
        await appClient.user.create({
          data: { username: body.admin.username.trim(), passwordHash },
        });
        adminCreated = true;
      }
    } catch (err) {
      const message = err instanceof Error ? err.message.split("\n")[0] : "Failed to create admin account";
      return NextResponse.json({ error: message }, { status: 500 });
    } finally {
      await appClient.$disconnect().catch(() => {});
    }

    // --- 4. Write .env (last step before declaring success) ----------------
    const forwardedProto = req.headers.get("x-forwarded-proto");
    const host = req.headers.get("host") || "localhost";
    const protocol = forwardedProto || req.nextUrl.protocol.replace(":", "") || "https";
    const nextAuthUrl = `${protocol}://${host}`;

    const envUpdates: Record<string, string> = {
      DATABASE_URL: databaseUrl,
      NEXTAUTH_URL: nextAuthUrl,
      NEXTAUTH_SECRET: generateSecureToken(32),
      CRON_SECRET: generateSecureToken(32),
    };

    if (body.mail?.webhookSigningKey) {
      envUpdates.INBOUND_WEBHOOK_SIGNING_KEY = body.mail.webhookSigningKey.trim();
    }
    if (body.mail?.mxHost) {
      envUpdates.INBOUND_MX_HOST = body.mail.mxHost.trim();
    }
    if (body.mail?.mxHostFallback) {
      envUpdates.INBOUND_MX_HOST_FALLBACK = body.mail.mxHostFallback.trim();
    }

    // INSTALL_COMPLETE is written last, deliberately — it's the flag every
    // other part of the app treats as "safe to use this configuration."
    envUpdates.INSTALL_COMPLETE = "true";

    updateEnvFile(envUpdates);

    // --- 5. Best-effort restart so the new .env actually takes effect ------
    // Env vars are only read at process boot, so the currently-running
    // process still has the OLD (placeholder) config until it restarts. We
    // don't know which process manager is in play, so we try both — each is
    // a harmless no-op if that mechanism isn't actually managing this app:
    //
    //   - PM2 (SSH + ecosystem.config.js deployments): `pm2 restart` talks
    //     to the separate PM2 daemon over IPC and returns once accepted.
    //   - Passenger (cPanel's "Setup Node.js App", no SSH needed): touching
    //     tmp/restart.txt is Passenger's documented signal to swap in a
    //     fresh process on the NEXT incoming request — which conveniently
    //     ends up being the wizard's own first status poll below.
    let restarted = false;
    let mechanism: "pm2" | "passenger" | "none" = "none";

    try {
      await execFileAsync("pm2", ["restart", "ecosystem.config.js"], {
        cwd: process.cwd(),
        timeout: 5_000,
      });
      restarted = true;
      mechanism = "pm2";
    } catch {
      // PM2 isn't managing this process (or isn't installed) — try Passenger.
      try {
        const tmpDir = path.join(process.cwd(), "tmp");
        fs.mkdirSync(tmpDir, { recursive: true });
        fs.writeFileSync(path.join(tmpDir, "restart.txt"), new Date().toISOString());
        restarted = true;
        mechanism = "passenger";
      } catch {
        restarted = false;
        mechanism = "none";
      }
    }

    return NextResponse.json({
      success: true,
      adminCreated,
      restarted,
      mechanism,
    });
  } finally {
    releaseLock();
  }
}
