import { createHash, randomBytes } from "node:crypto";
import { and, eq, inArray, isNull, sql } from "drizzle-orm";
import { cookies } from "next/headers";
import { db } from "@/db";
import { accountLifecycleEvents, authAccounts, authSessions, authUsers, pdnJobs, privacySettings, siteSettings, userEmails, userPhones } from "@/db/schema";
import { normalizePhone } from "@/lib/sms-pin";

export const SESSION_COOKIE = "kiln-session";
export const STATE_COOKIE_PREFIX = "kiln-oauth-state-";

const settingDefaults = {
  auth_enabled: process.env.AUTH_ENABLED === "true",
  auth_vk_enabled: process.env.AUTH_VK_ENABLED !== "false",
  auth_yandex_enabled: process.env.AUTH_YANDEX_ENABLED !== "false",
  auth_telegram_enabled: process.env.AUTH_TELEGRAM_ENABLED !== "false",
  auth_max_enabled: process.env.AUTH_MAX_ENABLED !== "false",
  orders_enabled: process.env.ORDERS_ENABLED === "true",
  orders_pickup_enabled: process.env.ORDERS_PICKUP_ENABLED !== "false",
  orders_deferred_enabled: process.env.ORDERS_DEFERRED_ENABLED !== "false",
  marketing_enabled: process.env.MARKETING_ENABLED === "true"
};
export type AuthProvider = "vk" | "yandex" | "telegram" | "max";
const CUSTOMER_PROVIDERS: AuthProvider[] = ["vk", "yandex", "telegram", "max"];

export class AuthIdentityError extends Error {
  code: "phone_required" | "account_suspended" | "identity_conflict";
  constructor(code: AuthIdentityError["code"]) { super(code); this.code = code; }
}

export async function isSettingEnabled(key: keyof typeof settingDefaults) {
  if (!db) return settingDefaults[key];
  const [row] = await db.select().from(siteSettings).where(eq(siteSettings.key, key)).limit(1);
  return row?.value ?? settingDefaults[key];
}

export function siteUrl(request?: Request) {
  if (process.env.NEXT_PUBLIC_SITE_URL) return process.env.NEXT_PUBLIC_SITE_URL.replace(/\/$/, "");
  if (request) {
    const forwardedHost = request.headers.get("x-forwarded-host")?.split(",")[0]?.trim();
    const host = forwardedHost || request.headers.get("host");
    const forwardedProto = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
    if (host) return `${forwardedProto || new URL(request.url).protocol.replace(":", "")}://${host}`;
    return new URL(request.url).origin;
  }
  return "http://localhost:3000";
}

export function createOAuthState() { return randomBytes(24).toString("hex"); }
export function createPkceVerifier() { return randomBytes(48).toString("base64url"); }
export function pkceChallenge(verifier: string) { return createHash("sha256").update(verifier).digest("base64url"); }
export function hashToken(token: string) { return createHash("sha256").update(token).digest("hex"); }

export async function createSession(userId: number) {
  if (!db) throw new Error("Database is not configured");
  const token = randomBytes(32).toString("base64url");
  await db.insert(authSessions).values({ tokenHash: hashToken(token), userId, expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) });
  return token;
}

export async function currentUser() {
  if (!db) return null;
  const token = (await cookies()).get(SESSION_COOKIE)?.value;
  if (!token) return null;
  const [row] = await db.select({ session: authSessions, user: authUsers }).from(authSessions).innerJoin(authUsers, eq(authUsers.id, authSessions.userId)).where(and(eq(authSessions.tokenHash, hashToken(token)), isNull(authSessions.revokedAt), eq(authUsers.state, "active"))).limit(1);
  if (!row || row.session.expiresAt < new Date()) return null;
  return row.user;
}

/** Resolves a customer by the provider-verified phone. The phone, never the
 * provider identity, owns the cabinet. All changes happen atomically. */
export async function resolvePhoneIdentity(provider: AuthProvider, providerAccountId: string, rawPhone: string, suggestedDisplayName?: string | null) {
  if (!db) throw new Error("Database is not configured");
  const phone = normalizePhone(rawPhone);
  if (!phone) throw new AuthIdentityError("phone_required");
  const suggestion = suggestedDisplayName?.trim().replace(/\s+/g, " ").slice(0, 64) || null;
  return db.transaction(async (tx) => {
    // Serialise all attempts for the same normalized phone.
    await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext(${phone}))`);
    const [phoneOwner] = await tx.select({ phone: userPhones, user: authUsers }).from(userPhones)
      .innerJoin(authUsers, eq(authUsers.id, userPhones.userId))
      .where(and(eq(userPhones.phone, phone), isNull(userPhones.archivedAt))).limit(1);
    const [identity] = await tx.select({ account: authAccounts, user: authUsers }).from(authAccounts)
      .innerJoin(authUsers, eq(authUsers.id, authAccounts.userId))
      .where(and(eq(authAccounts.provider, provider), eq(authAccounts.providerAccountId, providerAccountId), isNull(authAccounts.archivedAt))).limit(1);

    if (phoneOwner && phoneOwner.user.state !== "active") throw new AuthIdentityError("account_suspended");
    if (identity && identity.user.role !== "user") throw new AuthIdentityError("identity_conflict");
    if (identity && identity.user.state !== "active" && identity.user.suspendReason !== "merged_duplicate") throw new AuthIdentityError("account_suspended");
    let user = phoneOwner?.user;
    if (!user && identity?.user.state === "active") {
      const [identityPhone] = await tx.select().from(userPhones).where(and(eq(userPhones.userId, identity.user.id), isNull(userPhones.archivedAt))).limit(1);
      if (!identityPhone) user = identity.user;
    }
    if (!user) {
      [user] = await tx.insert(authUsers).values({ displayName: null, suggestedDisplayName: suggestion, role: "user" }).returning();
    } else if (!user.profileCompletedAt && suggestion && !user.suggestedDisplayName) {
      [user] = await tx.update(authUsers).set({ suggestedDisplayName: suggestion, updatedAt: new Date() }).where(eq(authUsers.id, user.id)).returning();
    }

    const [targetPhone] = await tx.select().from(userPhones).where(and(eq(userPhones.userId, user.id), isNull(userPhones.archivedAt))).limit(1);
    if (!targetPhone) await tx.insert(userPhones).values({ userId: user.id, phone, verifiedAt: new Date() });
    else if (targetPhone.phone !== phone) throw new AuthIdentityError("identity_conflict");

    if (identity && identity.user.id !== user.id) {
      const sourceId = identity.user.id;
      const now = new Date();
      const sourceAccounts = await tx.select().from(authAccounts).where(and(eq(authAccounts.userId, sourceId), isNull(authAccounts.archivedAt), inArray(authAccounts.provider, CUSTOMER_PROVIDERS)));
      for (const account of sourceAccounts) {
        await tx.update(authAccounts).set({ archivedAt: now }).where(eq(authAccounts.id, account.id));
        await tx.insert(authAccounts).values({ userId: user.id, provider: account.provider, providerAccountId: account.providerAccountId }).onConflictDoNothing();
      }
      const [targetEmail] = await tx.select().from(userEmails).where(and(eq(userEmails.userId, user.id), isNull(userEmails.archivedAt))).limit(1);
      let targetHasEmail = Boolean(targetEmail);
      const sourceEmails = await tx.select().from(userEmails).where(and(eq(userEmails.userId, sourceId), isNull(userEmails.archivedAt)));
      for (const email of sourceEmails) {
        if (!targetHasEmail) {
          await tx.update(userEmails).set({ userId: user.id, updatedAt: now }).where(eq(userEmails.id, email.id));
          targetHasEmail = true;
        } else await tx.update(userEmails).set({ archivedAt: now, updatedAt: now }).where(eq(userEmails.id, email.id));
      }
      await tx.update(userPhones).set({ archivedAt: now, updatedAt: now }).where(and(eq(userPhones.userId, sourceId), isNull(userPhones.archivedAt)));
      await tx.update(authSessions).set({ revokedAt: now }).where(and(eq(authSessions.userId, sourceId), isNull(authSessions.revokedAt)));
      const [retention] = await tx.select({ days: privacySettings.valueInteger }).from(privacySettings).where(eq(privacySettings.key, "empty_account_retention_days")).limit(1);
      const purgeAfter = new Date(now.getTime() + Math.max(30, retention?.days ?? 30) * 86400_000);
      await tx.update(authUsers).set({ state: "suspended", suspendedAt: now, suspendReason: "merged_duplicate", purgeAfter, mergedIntoUserId: user.id, updatedAt: now }).where(eq(authUsers.id, sourceId));
      await tx.insert(accountLifecycleEvents).values({ userId: sourceId, customerRef: identity.user.customerRef, eventType: "account_merged", reason: "verified_phone_match", details: { mergedIntoUserId: user.id, provider } });
      await tx.insert(pdnJobs).values({ userId: sourceId, customerRef: identity.user.customerRef, jobType: "suspend_remote", payload: { action: "merge" } });
    }

    const [activeIdentity] = await tx.select().from(authAccounts).where(and(eq(authAccounts.provider, provider), eq(authAccounts.providerAccountId, providerAccountId), isNull(authAccounts.archivedAt))).limit(1);
    if (!activeIdentity) await tx.insert(authAccounts).values({ userId: user.id, provider, providerAccountId });
    else if (activeIdentity.userId !== user.id) throw new AuthIdentityError("identity_conflict");
    return user;
  });
}

/** Finds the existing site account behind a trusted, already verified phone. */
export async function getUserByVerifiedPhone(phone: string) {
  if (!db) return null;
  const [row] = await db.select().from(authUsers)
    .innerJoin(userPhones, eq(userPhones.userId, authUsers.id))
    .where(and(eq(userPhones.phone, phone), sql`${userPhones.verifiedAt} IS NOT NULL`, isNull(userPhones.archivedAt), eq(authUsers.state, "active")))
    .limit(1);
  return row?.auth_users ?? null;
}
