/**
 * Registration — create an account and a profile in one step, gated on an
 * invite code.
 *
 * **Why this exists.** PIG shares its identity provider with another
 * application, and self-registration there is deliberately switched off. That
 * left the invite code unreachable: a new person with a personal address could
 * not obtain a token, so they could never get as far as the endpoint that
 * accepts the code. The gate was real but nothing could ever reach it.
 *
 * Opening self-registration on the provider would have opened it for the
 * neighbouring application too. So instead PIG mints the account itself, using
 * the provider's admin API, and **only after the invite code has been
 * validated**. The provider stays closed; the invite becomes the real gate.
 *
 * The order matters and is deliberate:
 *
 *   1. Validate the invite. No invite, nothing happens at all.
 *   2. Create the identity-provider account.
 *   3. Create the PIG profile and team membership.
 *   4. Consume the invite.
 *
 * If step 3 fails the provider account is removed again, so a half-registered
 * person cannot exist — able to sign in, but with no profile and no way to get
 * one because the invite is spent.
 */
import { Hono } from 'hono';
import { and, eq, gt, isNull, or, sql } from 'drizzle-orm';
import { z } from 'zod';
import { createHash } from 'node:crypto';
import type { Database } from '@pig/db';
import { invites, teamMemberships, users } from '@pig/db';
import { TEAMS } from '@pig/core';
import type { Config } from '../lib/config';

const bodySchema = z.object({
  email: z.string().email().max(200),
  // Eight is the floor. The provider's own default is six, which is too short
  // for an account that reaches commercial data.
  password: z.string().min(8, 'Use at least 8 characters.').max(200),
  name: z.string().min(1).max(120),
  team: z.enum(TEAMS),
  inviteCode: z.string().min(1).max(200),
  title: z.string().max(160).optional(),
});

export function createRegisterRoute(config: Config, db: Database) {
  const app = new Hono();

  app.post('/api/register', async (c) => {
    if (!config.SUPABASE_URL || !config.SUPABASE_SERVICE_KEY) {
      // Without the admin credential PIG cannot mint accounts. Say so plainly
      // rather than failing in a way that looks like the invite is wrong.
      return c.json(
        {
          error:
            'Self-registration is not configured on this deployment. Ask an ' +
            'administrator to create your account.',
          code: 'registration_unavailable',
        },
        503,
      );
    }

    const parsed = bodySchema.safeParse(await c.req.json().catch(() => ({})));
    if (!parsed.success) {
      return c.json(
        { error: parsed.error.issues[0]?.message ?? 'Invalid details', code: 'invalid_body' },
        400,
      );
    }

    const email = parsed.data.email.trim().toLowerCase();

    // --- 1. The invite, before anything is created anywhere ----------------
    //
    // An administrator's address does NOT bypass this. The bypass in
    // /api/signup exists to bootstrap the first admin from an account that
    // already exists; here we are creating an account from nothing, and an
    // ungated version of that is an open registration endpoint.
    const codeHash = createHash('sha256').update(parsed.data.inviteCode.trim()).digest('hex');

    const [invite] = await db
      .select()
      .from(invites)
      .where(
        and(
          eq(invites.codeHash, codeHash),
          isNull(invites.revokedAt),
          gt(invites.usesRemaining, 0),
        ),
      )
      .limit(1);

    if (!invite) {
      return c.json({ error: 'That invite code is not valid.', code: 'invalid_invite' }, 403);
    }
    if (invite.expiresAt && invite.expiresAt < new Date()) {
      return c.json({ error: 'That invite code has expired.', code: 'expired_invite' }, 403);
    }
    if (invite.email && invite.email.toLowerCase() !== email) {
      return c.json(
        { error: 'That invite code was issued to a different address.', code: 'invite_mismatch' },
        403,
      );
    }

    // Already a PIG member? Send them to sign in rather than creating a second
    // account they cannot use.
    const [existingProfile] = await db
      .select({ id: users.id })
      .from(users)
      .where(eq(users.email, email))
      .limit(1);

    if (existingProfile) {
      return c.json(
        { error: 'That address is already a member. Sign in instead.', code: 'already_member' },
        409,
      );
    }

    // --- 2. The identity-provider account ---------------------------------
    const adminHeaders = {
      apikey: config.SUPABASE_SERVICE_KEY,
      authorization: `Bearer ${config.SUPABASE_SERVICE_KEY}`,
      'content-type': 'application/json',
    };

    let authSubject: string;
    try {
      const response = await fetch(`${config.SUPABASE_URL}/auth/v1/admin/users`, {
        method: 'POST',
        headers: adminHeaders,
        body: JSON.stringify({
          email,
          password: parsed.data.password,
          // Confirmed on creation: the invite already proved this person was
          // vouched for, and requiring a second confirmation email would strand
          // anyone whose address cannot receive mail from this project.
          email_confirm: true,
        }),
      });

      if (!response.ok) {
        const detail = await response.text().catch(() => '');
        // An address that already exists in the provider but has no PIG
        // profile is a real case — someone with an account from the sibling
        // application. Direct them to sign in and complete a profile, which
        // /api/signup handles.
        if (response.status === 422 || detail.includes('already been registered')) {
          return c.json(
            {
              error:
                'An account already exists for that address. Sign in with it, and ' +
                'you will be asked to set up your PIG profile.',
              code: 'account_exists',
            },
            409,
          );
        }
        console.error('[pig] provider rejected account creation:', response.status, detail.slice(0, 200));
        return c.json({ error: 'Could not create the account.', code: 'provider_error' }, 502);
      }

      const created = (await response.json()) as { id?: string };
      if (!created.id) throw new Error('provider returned no user id');
      authSubject = created.id;
    } catch (error) {
      console.error('[pig] account creation failed:', error);
      return c.json({ error: 'Could not create the account.', code: 'provider_error' }, 502);
    }

    // --- 3. The PIG profile ------------------------------------------------
    try {
      const [user] = await db
        .insert(users)
        .values({
          authSubject,
          email,
          name: parsed.data.name,
          title: parsed.data.title,
          // Admin rights come only from server configuration, never from a
          // registration payload.
          isPlatformAdmin: config.adminEmails.includes(email),
        })
        .returning();

      if (!user) throw new Error('profile insert returned nothing');

      await db
        .insert(teamMemberships)
        .values({
          userId: user.id,
          team: parsed.data.team,
          role: config.adminEmails.includes(email) ? 'admin' : 'member',
          isPrimary: true,
        })
        .onConflictDoNothing();

      // --- 4. Consume the invite -------------------------------------------
      await db
        .update(invites)
        .set({
          redeemedByUserId: user.id,
          redeemedAt: new Date(),
          usesRemaining: sql`GREATEST(0, ${invites.usesRemaining} - 1)`,
        })
        .where(eq(invites.id, invite.id));

      return c.json({ ok: true, email: user.email }, 201);
    } catch (error) {
      // Roll the provider account back. Without this, the person could sign in
      // but would have no profile and no way to obtain one, because their
      // invite would look spent — a genuinely stuck state.
      console.error('[pig] profile creation failed, removing the auth account:', error);
      await fetch(`${config.SUPABASE_URL}/auth/v1/admin/users/${authSubject}`, {
        method: 'DELETE',
        headers: adminHeaders,
      }).catch(() => {});
      return c.json({ error: 'Could not complete registration.', code: 'profile_error' }, 500);
    }
  });

  return app;
}
