import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { nextCookies } from "better-auth/next-js";
import { magicLink } from "better-auth/plugins/magic-link";
import { and, desc, eq, isNull } from "drizzle-orm";
import { Resend } from "resend";
import { account, auditJob, db, lead, session, user, verification } from "@/db";
import { claimFromCookieHeader, intentFromCookieHeader } from "./claim";
import { allowedOrigins } from "./sites";

/**
 * The public URL of this app, or nothing in development.
 *
 * Every magic link and every verification redirect is built from it, so it has
 * to be the address of the server that issued the link. In production that is
 * one fixed domain and `BETTER_AUTH_URL` names it.
 *
 * Locally it is not fixed — d3k picks the port — and the variable, on this
 * machine, holds the production URL both in `.env.local` and in the shell. The
 * link then pointed at `seo.seoinsight.app`, so clicking it left the server that
 * had just minted the token and the flow could not be finished at all. Returning
 * nothing lets better-auth read the host off the request instead, which is
 * always the right one and is one fewer variable to keep in step with a URL that
 * changes on its own.
 */
function baseUrl(): string | undefined {
  if (process.env.NODE_ENV !== "production") return undefined;
  return process.env.BETTER_AUTH_URL;
}

/**
 * Lazily constructed: `new Resend()` throws on a missing key, and that must
 * happen when an email is actually sent, not when the module is first imported
 * — otherwise `next build` fails on a machine that has no mail credentials.
 */
let resend: Resend | null = null;
function mailer(): Resend {
  const key = process.env.RESEND_API_KEY;
  if (!key) throw new Error("RESEND_API_KEY is not set — see .env.example");
  return (resend ??= new Resend(key));
}

/**
 * Record the lead, and hand this browser's pending audits to the account that
 * just appeared.
 *
 * Run on session creation rather than user creation, so that a returning
 * visitor who ran a second audit while signed out also gets it. Everything here
 * is idempotent: the update only touches unclaimed jobs, and the lead is
 * written once per address and site.
 *
 * The lead is written whether or not anything was claimed. The address is what
 * the visitor traded for the report — losing it because a claim cookie went
 * missing would mean the trade happened and we kept no side of it.
 */
async function onSignIn(
  userId: string,
  cookieHeader: string | null,
  host: string | null,
): Promise<void> {
  // Read the address back rather than trusting a field off the session shape:
  // the row is the authority on who this user is.
  const owner = await db
    .select({ email: user.email })
    .from(user)
    .where(eq(user.id, userId))
    .limit(1);
  const email = owner[0]?.email;
  if (!email) return;

  // The claim cookie says which audits this browser ran; the intent cookie says
  // which address this browser asked a link for. Only both together mean "the
  // visitor who ran these audits is the one signing in" — a link requested by
  // somebody else and clicked here creates a session for their address, which
  // no intent of ours vouches for, and claims nothing.
  const token = claimFromCookieHeader(cookieHeader);
  const intended = intentFromCookieHeader(cookieHeader);

  const claimed =
    token !== null && intended === email.toLowerCase()
      ? await db
          .update(auditJob)
          .set({ userId })
          .where(and(eq(auditJob.claimToken, token), isNull(auditJob.userId)))
          .returning({ id: auditJob.id, site: auditJob.site })
      : [];

  // A lead belongs to the site whose form the visitor used, which only the jobs
  // know — the sign-in itself happened on the dashboard, wherever they came
  // from. With nothing claimed, the dashboard is all we can honestly say.
  // Read off the request rather than off `BETTER_AUTH_URL`, which no longer
  // exists in development — and which was never the more reliable of the two.
  const site = claimed[0]?.site ?? host ?? "inconnu";
  const existing = await db
    .select({ id: lead.id })
    .from(lead)
    .where(and(eq(lead.email, email), eq(lead.site, site)))
    .orderBy(desc(lead.createdAt))
    .limit(1);
  if (existing.length === 0) {
    await db.insert(lead).values({ email, site, userId });
  }
}

export const auth = betterAuth({
  // The adapter looks its four tables up by name, so it is handed exactly
  // those — the queue tables are ours and none of its business.
  database: drizzleAdapter(db, {
    provider: "pg",
    schema: { user, session, account, verification },
  }),

  // No password, no OAuth: the only way in is a link in an email, which is also
  // the address we need anyway to send the report.
  emailAndPassword: { enabled: false },

  baseURL: baseUrl(),
  trustedOrigins: allowedOrigins(),

  databaseHooks: {
    session: {
      create: {
        after: async (created, context) => {
          // `context` is null when a session is created outside a request, and
          // the cookies only exist inside one.
          const headers = context?.headers ?? null;
          await onSignIn(
            created.userId,
            headers?.get("cookie") ?? null,
            headers?.get("host") ?? null,
          );
        },
      },
    },
  },

  plugins: [
    magicLink({
      // Long enough to switch to a mail client, short enough that a link
      // forwarded or left in an inbox stops working.
      expiresIn: 15 * 60,
      sendMagicLink: async ({ email, url }) => {
        // Development gets the link on the server log, where d3k puts it next
        // to everything else. Not a shortcut around the flow — the token, the
        // expiry and the verification are the same ones production uses; only
        // the delivery differs, and locally it cannot work anyway: the sender
        // domain is unverified, so Resend accepts the call and delivers to
        // nobody. Without this the flow simply cannot be walked here.
        if (process.env.NODE_ENV !== "production") {
          console.log(`auth: magic link for ${email} — ${url}`);
        }

        try {
          await mailer().emails.send({
            from: process.env.RESEND_FROM ?? "SEO Insight <onboarding@resend.dev>",
            to: email,
            subject: "Votre rapport SEO",
            text: `Ouvrez ce lien pour voir votre rapport : ${url}\n\nIl expire dans 15 minutes.`,
            html: `<p>Ouvrez ce lien pour voir votre rapport&nbsp;:</p>
<p><a href="${url}">${url}</a></p>
<p>Il expire dans 15 minutes. Si vous n'avez rien demandé, ignorez cet email.</p>`,
          });
        } catch (failure) {
          // The send still runs in development, so the call that ships is the
          // call we exercise — but a missing key or an unverified domain must
          // not be what stops the flow when the link is already on the log.
          if (process.env.NODE_ENV === "production") throw failure;
          console.warn("auth: the email did not go out — use the link above", failure);
        }
      },
    }),
    // Must stay last: it writes the Set-Cookie header Next needs, and any
    // plugin registered after it would run too late to be included.
    nextCookies(),
  ],
});
