import bcrypt from "bcryptjs";
import { eq } from "drizzle-orm";
import { z } from "zod/v4";
import { db, pool } from "@workspace/db";
import {
  systemSettingsTable,
  usersTable,
  rolesTable,
  userRolesTable,
} from "@workspace/db";

// Read app version from package.json at startup. Falls back to "0.0.0".
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import path from "node:path";

let cachedVersion: string | null = null;
function readAppVersion(): string {
  if (cachedVersion) return cachedVersion;
  try {
    const here = path.dirname(fileURLToPath(import.meta.url));
    // Walk up from dist/ or src/ to the api-server package.json.
    for (const candidate of [
      path.resolve(here, "../../package.json"),
      path.resolve(here, "../package.json"),
      path.resolve(process.cwd(), "package.json"),
    ]) {
      try {
        const raw = readFileSync(candidate, "utf8");
        const parsed = JSON.parse(raw) as { version?: string };
        if (parsed.version) {
          cachedVersion = parsed.version;
          return cachedVersion;
        }
      } catch {
        // try next candidate
      }
    }
  } catch {
    // ignore
  }
  cachedVersion = "0.0.0";
  return cachedVersion;
}

export const SetupCompleteBody = z.object({
  admin: z.object({
    username: z
      .string()
      .min(3)
      .max(50)
      .regex(/^[a-zA-Z0-9_.-]+$/, "username_invalid"),
    email: z.string().email().max(255),
    password: z.string().min(8).max(200),
    displayNameAr: z.string().min(1).max(200).optional(),
    displayNameEn: z.string().min(1).max(200).optional(),
  }),
  baseUrl: z.string().url().optional(),
  localDomain: z.string().min(1).max(200).optional(),
  localIp: z.string().min(1).max(64).optional(),
  httpsMode: z.enum(["local", "byo", "skip"]).default("local").optional(),
});
export type SetupCompleteInput = z.infer<typeof SetupCompleteBody>;

export const SetupValidateBody = SetupCompleteBody.partial({
  admin: true,
}).extend({
  admin: SetupCompleteBody.shape.admin.partial(),
});

export type SetupStatus = {
  installed: boolean;
  setupRequired: boolean;
  appVersion: string;
  checks: {
    db: "ok" | "error";
    storage: "ok" | "unknown";
    https: "ok" | "skip" | "unknown";
    baseUrl: string | null;
  };
};

async function getSystemSettings(): Promise<{
  installed: boolean;
  baseUrl: string | null;
  httpsMode: string;
} | null> {
  const rows = await db
    .select()
    .from(systemSettingsTable)
    .where(eq(systemSettingsTable.id, 1))
    .limit(1);
  const row = rows[0];
  if (!row) return null;
  return {
    installed: row.installed,
    baseUrl: row.baseUrl,
    httpsMode: row.httpsMode,
  };
}

async function adminExists(): Promise<boolean> {
  const rows = await db
    .select({ id: usersTable.id })
    .from(userRolesTable)
    .innerJoin(usersTable, eq(usersTable.id, userRolesTable.userId))
    .innerJoin(rolesTable, eq(rolesTable.id, userRolesTable.roleId))
    .where(eq(rolesTable.name, "admin"))
    .limit(1);
  return rows.length > 0;
}

export async function readStatus(): Promise<SetupStatus> {
  let dbCheck: "ok" | "error" = "ok";
  let installed = false;
  let baseUrl: string | null = null;
  let httpsMode = process.env.HTTPS_MODE ?? "local";
  try {
    const sys = await getSystemSettings();
    if (sys) {
      installed = sys.installed;
      baseUrl = sys.baseUrl;
      httpsMode = sys.httpsMode;
    }
  } catch {
    dbCheck = "error";
  }

  let setupRequired = false;
  if (dbCheck === "ok") {
    if (installed) {
      setupRequired = false;
    } else {
      try {
        setupRequired = !(await adminExists());
      } catch {
        dbCheck = "error";
        setupRequired = false;
      }
    }
  }

  const storage =
    process.env.STORAGE_DRIVER === "local" || process.env.STORAGE_DRIVER === "s3"
      ? "ok"
      : "unknown";
  const https =
    httpsMode === "skip"
      ? "skip"
      : (process.env.PUBLIC_BASE_URL ?? "").startsWith("https://")
        ? "ok"
        : "unknown";

  return {
    installed,
    setupRequired,
    appVersion: readAppVersion(),
    checks: {
      db: dbCheck,
      storage,
      https,
      baseUrl: baseUrl ?? process.env.PUBLIC_BASE_URL ?? null,
    },
  };
}

export type ValidationErrors = Record<string, string>;

export async function validateProposed(
  input: unknown,
): Promise<{ ok: true } | { ok: false; errors: ValidationErrors }> {
  const parsed = SetupValidateBody.safeParse(input);
  if (!parsed.success) {
    const errors: ValidationErrors = {};
    for (const issue of parsed.error.issues) {
      errors[issue.path.join(".") || "_"] = issue.message;
    }
    return { ok: false, errors };
  }
  const data = parsed.data;
  const errors: ValidationErrors = {};
  if (data.admin?.username) {
    const existing = await db
      .select({ id: usersTable.id })
      .from(usersTable)
      .where(eq(usersTable.username, data.admin.username))
      .limit(1);
    if (existing.length > 0) errors["admin.username"] = "username_taken";
  }
  if (data.admin?.email) {
    const existing = await db
      .select({ id: usersTable.id })
      .from(usersTable)
      .where(eq(usersTable.email, data.admin.email))
      .limit(1);
    if (existing.length > 0) errors["admin.email"] = "email_taken";
  }
  if (Object.keys(errors).length > 0) return { ok: false, errors };
  return { ok: true };
}

export type CompleteResult =
  | { ok: true; userId: number }
  | { ok: false; status: 409 | 400; error: string; errors?: ValidationErrors };

export async function completeInstall(
  rawInput: unknown,
): Promise<CompleteResult> {
  const parsed = SetupCompleteBody.safeParse(rawInput);
  if (!parsed.success) {
    const errors: ValidationErrors = {};
    for (const issue of parsed.error.issues) {
      errors[issue.path.join(".") || "_"] = issue.message;
    }
    return { ok: false, status: 400, error: "validation_failed", errors };
  }
  const input = parsed.data;
  const passwordHash = await bcrypt.hash(input.admin.password, 10);

  // Wrap the whole flow in a single SQL transaction so that admin
  // creation + system_settings flip + role/group assignments either
  // all succeed or all roll back.
  const client = await pool.connect();
  try {
    await client.query("BEGIN");

    // Serialize concurrent /api/setup/complete callers. Without this two
    // parallel requests could both observe installed=false / no admin and
    // both insert distinct first admins. The advisory lock is held until
    // COMMIT/ROLLBACK so the second caller blocks, then re-reads the
    // gates below and bails with 409.
    // Constant lock key dedicated to first-time setup; arbitrary but stable.
    await client.query(`SELECT pg_advisory_xact_lock(8473123001)`);

    // Re-check both gates inside the txn so a parallel request can't
    // sneak past the open setup window.
    const sysRows = await client.query<{
      installed: boolean;
    }>(`SELECT installed FROM system_settings WHERE id = 1 LIMIT 1`);
    if (sysRows.rows[0]?.installed) {
      await client.query("ROLLBACK");
      return { ok: false, status: 409, error: "already_installed" };
    }

    const adminCount = await client.query<{ id: number }>(
      `SELECT u.id FROM users u
         JOIN user_roles ur ON ur.user_id = u.id
         JOIN roles r ON r.id = ur.role_id
         WHERE r.name = 'admin' LIMIT 1`,
    );
    if ((adminCount.rowCount ?? 0) > 0) {
      await client.query("ROLLBACK");
      return { ok: false, status: 409, error: "already_installed" };
    }

    // Username/email uniqueness re-check inside the txn. We map
    // collisions back to per-field keys so the wizard can highlight
    // the offending input precisely.
    const dup = await client.query<{
      id: number;
      username: string;
      email: string;
    }>(
      `SELECT id, username, email FROM users WHERE username = $1 OR email = $2`,
      [input.admin.username, input.admin.email],
    );
    if ((dup.rowCount ?? 0) > 0) {
      await client.query("ROLLBACK");
      const errors: ValidationErrors = {};
      for (const row of dup.rows) {
        if (row.username === input.admin.username)
          errors["admin.username"] = "username_taken";
        if (row.email === input.admin.email)
          errors["admin.email"] = "email_taken";
      }
      return {
        ok: false,
        status: 400,
        error: "validation_failed",
        errors,
      };
    }

    // Ensure the admin role exists (it normally does after seed).
    await client.query(
      `INSERT INTO roles (name, description_ar, description_en, is_system)
       VALUES ('admin', 'مدير النظام', 'System Administrator', 1)
       ON CONFLICT (name) DO NOTHING`,
    );
    const roleRow = await client.query<{ id: number }>(
      `SELECT id FROM roles WHERE name = 'admin' LIMIT 1`,
    );
    const adminRoleId = roleRow.rows[0]?.id;
    if (!adminRoleId) {
      await client.query("ROLLBACK");
      return { ok: false, status: 400, error: "admin_role_missing" };
    }

    const inserted = await client.query<{ id: number }>(
      `INSERT INTO users
         (username, email, password_hash, display_name_ar, display_name_en, preferred_language, is_active)
       VALUES ($1, $2, $3, $4, $5, 'ar', true)
       RETURNING id`,
      [
        input.admin.username,
        input.admin.email,
        passwordHash,
        input.admin.displayNameAr ?? "مدير النظام",
        input.admin.displayNameEn ?? "System Admin",
      ],
    );
    const newUserId = inserted.rows[0]!.id;

    await client.query(
      `INSERT INTO user_roles (user_id, role_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
      [newUserId, adminRoleId],
    );

    // Best-effort: also map into Admins + Everyone groups if they exist.
    await client.query(
      `INSERT INTO user_groups (user_id, group_id)
         SELECT $1, id FROM groups WHERE name IN ('Admins','Everyone')
       ON CONFLICT DO NOTHING`,
      [newUserId],
    );

    // Upsert system_settings row → installed=true.
    await client.query(
      `INSERT INTO system_settings
         (id, installed, installed_at, base_url, local_domain, local_ip, https_mode, app_version, updated_at)
       VALUES (1, true, NOW(), $1, $2, $3, $4, $5, NOW())
       ON CONFLICT (id) DO UPDATE SET
         installed = true,
         installed_at = COALESCE(system_settings.installed_at, NOW()),
         base_url = EXCLUDED.base_url,
         local_domain = EXCLUDED.local_domain,
         local_ip = EXCLUDED.local_ip,
         https_mode = EXCLUDED.https_mode,
         app_version = EXCLUDED.app_version,
         updated_at = NOW()`,
      [
        input.baseUrl ?? process.env.PUBLIC_BASE_URL ?? null,
        input.localDomain ?? process.env.LOCAL_DOMAIN ?? null,
        input.localIp ?? process.env.LOCAL_IP ?? null,
        input.httpsMode ?? process.env.HTTPS_MODE ?? "local",
        readAppVersion(),
      ],
    );

    await client.query("COMMIT");
    return { ok: true, userId: newUserId };
  } catch (err) {
    try {
      await client.query("ROLLBACK");
    } catch {
      /* ignore */
    }
    throw err;
  } finally {
    client.release();
  }
}

// Bootstrap idempotently called from index.ts at server start. Ensures
// system_settings(id=1) exists and backfills installed=true for legacy
// installs where an admin already exists. Migration-safe: runs on every
// boot regardless of whether the seed script was invoked.
export async function ensureSystemSettingsBootstrap(): Promise<void> {
  const client = await pool.connect();
  try {
    await client.query(
      `INSERT INTO system_settings (id, installed)
         VALUES (1, false)
       ON CONFLICT (id) DO NOTHING`,
    );
    const sys = await client.query<{ installed: boolean }>(
      `SELECT installed FROM system_settings WHERE id = 1 LIMIT 1`,
    );
    if (sys.rows[0]?.installed) return;
    const adminCount = await client.query<{ id: number }>(
      `SELECT u.id FROM users u
         JOIN user_roles ur ON ur.user_id = u.id
         JOIN roles r ON r.id = ur.role_id
         WHERE r.name = 'admin' LIMIT 1`,
    );
    if ((adminCount.rowCount ?? 0) > 0) {
      await client.query(
        `UPDATE system_settings
            SET installed = true,
                installed_at = COALESCE(installed_at, NOW()),
                updated_at = NOW()
          WHERE id = 1`,
      );
    }
  } finally {
    client.release();
  }
}

// Should the SPA router redirect to /setup? Mirrors readStatus().
export async function isSetupOpen(): Promise<boolean> {
  try {
    const sys = await getSystemSettings();
    if (sys?.installed) return false;
    return !(await adminExists());
  } catch {
    return false;
  }
}

// Returns the full SetupStatus + a single redirect decision so callers
// don't have to make two requests.
export async function redirectIfSetupNeeded(): Promise<{
  shouldRedirect: boolean;
  target: "/setup" | null;
  status: SetupStatus;
}> {
  const status = await readStatus();
  const shouldRedirect =
    status.checks.db === "ok" && !status.installed && status.setupRequired;
  return {
    shouldRedirect,
    target: shouldRedirect ? "/setup" : null,
    status,
  };
}

// Exported for use by tests; unused at runtime.
export const _internal = { adminExists, getSystemSettings, readAppVersion };
