import { nip19 } from 'nostr-tools';
import { z } from 'zod';

import { AppController } from '@/app.ts';
import { Conf } from '@/config.ts';
import { Storages } from '@/storages.ts';
import { parseBody } from '@/utils/api.ts';
import { getTokenHash } from '@/utils/auth.ts';

/** https://docs.joinmastodon.org/entities/WebPushSubscription/ */
interface MastodonPushSubscription {
  id: string;
  endpoint: string;
  server_key: string;
  alerts: Record<string, boolean>;
  policy: 'all' | 'followed' | 'follower' | 'none';
}

const pushSubscribeSchema = z.object({
  subscription: z.object({
    endpoint: z.string().url(),
    keys: z.object({
      p256dh: z.string(),
      auth: z.string(),
    }),
  }),
  data: z.object({
    alerts: z.object({
      mention: z.boolean().optional(),
      status: z.boolean().optional(),
      reblog: z.boolean().optional(),
      follow: z.boolean().optional(),
      follow_request: z.boolean().optional(),
      favourite: z.boolean().optional(),
      poll: z.boolean().optional(),
      update: z.boolean().optional(),
      'admin.sign_up': z.boolean().optional(),
      'admin.report': z.boolean().optional(),
    }).optional(),
    policy: z.enum(['all', 'followed', 'follower', 'none']).optional(),
  }).optional(),
});

export const pushSubscribeController: AppController = async (c) => {
  const vapidPublicKey = await Conf.vapidPublicKey;

  if (!vapidPublicKey) {
    return c.json({ error: 'The administrator of this server has not enabled Web Push notifications.' }, 404);
  }

  const accessToken = getAccessToken(c.req.raw);

  const kysely = await Storages.kysely();
  const signer = c.get('signer')!;

  const result = pushSubscribeSchema.safeParse(await parseBody(c.req.raw));

  if (!result.success) {
    return c.json({ error: 'Invalid request', schema: result.error }, 400);
  }

  const { subscription, data } = result.data;

  const pubkey = await signer.getPublicKey();
  const tokenHash = await getTokenHash(accessToken as `token1${string}`);

  const { id } = await kysely.transaction().execute(async (trx) => {
    await trx
      .deleteFrom('push_subscriptions')
      .where('token_hash', '=', tokenHash)
      .execute();

    return trx
      .insertInto('push_subscriptions')
      .values({
        pubkey,
        token_hash: tokenHash,
        endpoint: subscription.endpoint,
        p256dh: subscription.keys.p256dh,
        auth: subscription.keys.auth,
        data,
      })
      .returning('id')
      .executeTakeFirstOrThrow();
  });

  return c.json(
    {
      id: id.toString(),
      endpoint: subscription.endpoint,
      alerts: data?.alerts ?? {},
      policy: data?.policy ?? 'all',
      server_key: vapidPublicKey,
    } satisfies MastodonPushSubscription,
  );
};

export const getSubscriptionController: AppController = async (c) => {
  const vapidPublicKey = await Conf.vapidPublicKey;

  if (!vapidPublicKey) {
    return c.json({ error: 'The administrator of this server has not enabled Web Push notifications.' }, 404);
  }

  const accessToken = getAccessToken(c.req.raw);

  const kysely = await Storages.kysely();
  const tokenHash = await getTokenHash(accessToken as `token1${string}`);

  const row = await kysely
    .selectFrom('push_subscriptions')
    .selectAll()
    .where('token_hash', '=', tokenHash)
    .executeTakeFirstOrThrow();

  return c.json(
    {
      id: row.id.toString(),
      endpoint: row.endpoint,
      alerts: row.data?.alerts ?? {},
      policy: row.data?.policy ?? 'all',
      server_key: vapidPublicKey,
    } satisfies MastodonPushSubscription,
  );
};

/** Get access token from HTTP headers, but only if it's a `token1`. Otherwise return undefined. */
function getAccessToken(request: Request): `token1${string}` | undefined {
  const BEARER_REGEX = new RegExp(`^Bearer (${nip19.BECH32_REGEX.source})$`);

  const authorization = request.headers.get('authorization');
  const match = authorization?.match(BEARER_REGEX);

  const [_, accessToken] = match ?? [];

  if (accessToken?.startsWith('token1')) {
    return accessToken as `token1${string}`;
  }
}
