import { readFile } from 'node:fs/promises';
import { Notification, Provider } from '@parse/node-apn';
import webpush from 'web-push';
import { z } from 'zod';
import { getEnv } from '../config/env.js';
import { db } from '../db/client.js';
import { ensureNotificationMessagesTable } from './notification-message-service.js';

export const NOTIFICATION_TOKEN_TABLE = 'notification_tokens';
export const WEB_PUSH_SUBSCRIPTION_TABLE = 'web_push_subscriptions';
export const NOTIFICATION_PREFERENCE_TABLE = 'notification_preferences';

const automationNotificationPreferenceSchema = z.object({
  notifyOnAutomationStart: z.boolean().optional(),
  notifyOnAutomationProgress: z.boolean().optional(),
  notifyOnAutomationCompletion: z.boolean().optional(),
  notifyOnAutomationRelease: z.boolean().optional(),
  notifyOnAutomationMain: z.boolean().optional(),
  notifyOnAutomationFailure: z.boolean().optional(),
  notifyOnAutomationRestart: z.boolean().optional(),
  notifyOnAutomationIssueResolved: z.boolean().optional(),
});

const notificationTargetKindSchema = z.enum(['client', 'ios-token', 'ios-token-client', 'web-endpoint']);

export const registerAutomationNotificationPreferenceSchema = z.object({
  targetKind: notificationTargetKindSchema.default('client'),
  targetId: z.string().trim().min(1).max(1000).optional(),
  automation: automationNotificationPreferenceSchema,
});

export const registerIosTokenSchema = z.object({
  token: z.string().trim().min(1),
  deviceId: z.string().trim().min(1).max(200).optional(),
  appOrigin: z.string().trim().url().max(500).optional(),
  appDomain: z.string().trim().min(1).max(255).optional(),
  enabled: z.boolean().default(true),
});

export const unregisterIosTokenSchema = z.object({
  token: z.string().trim().min(1),
});

export const registerWebPushSubscriptionSchema = z.object({
  subscription: z.object({
    endpoint: z.string().trim().url(),
    expirationTime: z.number().nullable().optional(),
    keys: z.object({
      p256dh: z.string().trim().min(1),
      auth: z.string().trim().min(1),
    }),
  }),
  deviceId: z.string().trim().min(1).max(200).optional(),
  clientId: z.string().trim().min(1).max(200).optional(),
  userAgent: z.string().trim().max(500).optional(),
  appOrigin: z.string().trim().url().max(500).optional(),
  appDomain: z.string().trim().min(1).max(255).optional(),
  enabled: z.boolean().default(true),
});

export const unregisterWebPushSubscriptionSchema = z.object({
  endpoint: z.string().trim().url(),
});

export const sendIosNotificationSchema = z.object({
  title: z.string().trim().min(1),
  body: z.string().trim().min(1),
  data: z.record(z.string(), z.string()).default({}),
  threadId: z.string().trim().min(1).optional(),
  targetDeviceIds: z.array(z.string().trim().min(1).max(200)).max(50).optional(),
  targetClientIds: z.array(z.string().trim().min(1).max(200)).max(50).optional(),
  targetAppOrigins: z.array(z.string().trim().url().max(500)).max(50).optional(),
  targetAppDomains: z.array(z.string().trim().min(1).max(255)).max(50).optional(),
});

type IosNotificationPayload = z.infer<typeof sendIosNotificationSchema>;
type WebPushSubscriptionPayload = z.infer<typeof registerWebPushSubscriptionSchema>['subscription'];
type AutomationNotificationPreference = z.infer<typeof automationNotificationPreferenceSchema>;
type NotificationTargetKind = z.infer<typeof notificationTargetKindSchema>;
type NotificationPreferenceTarget = {
  kind: NotificationTargetKind;
  id: string;
};

function normalizeTargetDeviceIds(payload: {
  targetDeviceIds?: string[];
}) {
  return [...new Set((payload.targetDeviceIds ?? []).map((value) => String(value ?? '').trim()).filter(Boolean))];
}

function normalizeTargetClientIds(payload: {
  targetClientIds?: string[];
}) {
  return [...new Set((payload.targetClientIds ?? []).map((value) => String(value ?? '').trim()).filter(Boolean))];
}

function normalizeRegistrationCleanupIds(...values: Array<string | undefined>) {
  return [...new Set(values.map((value) => String(value ?? '').trim()).filter(Boolean))];
}

async function removeLegacyWebPushSubscriptionsForRegistration(args: {
  endpoint: string;
  deviceId?: string;
  clientId?: string;
  userAgent?: string;
  appOrigin?: string;
}) {
  const appOrigin = normalizeAppOrigin(args.appOrigin);
  const userAgent = String(args.userAgent ?? '').trim();
  const deviceId = String(args.deviceId ?? '').trim();
  const clientId = String(args.clientId ?? '').trim();

  if (!appOrigin || !userAgent || !deviceId || !clientId || deviceId === clientId) {
    return 0;
  }

  return db(WEB_PUSH_SUBSCRIPTION_TABLE)
    .whereNot({ endpoint: args.endpoint })
    .andWhere({ app_origin: appOrigin, user_agent: userAgent })
    .whereNull('client_id')
    .whereNotIn('device_id', [deviceId, clientId])
    .delete();
}

function normalizeTargetAppOrigins(targetAppOrigins: string[] | undefined) {
  return [...new Set((targetAppOrigins ?? []).map((value) => normalizeAppOrigin(value)).filter(Boolean))];
}

function normalizeTargetAppDomains(targetAppDomains: string[] | undefined) {
  return [...new Set((targetAppDomains ?? []).map((value) => normalizeAppDomain(value)).filter(Boolean))];
}

function isAllowedTargetRecipient(
  target: {
    deviceId?: string;
    clientId?: string;
  },
  targetDeviceIds: string[],
  targetClientIds: string[],
) {
  if (targetDeviceIds.length === 0 && targetClientIds.length === 0) {
    return true;
  }

  const deviceId = String(target.deviceId ?? '').trim();
  const clientId = String(target.clientId ?? '').trim();
  return (Boolean(deviceId) && targetDeviceIds.includes(deviceId)) || (Boolean(clientId) && targetClientIds.includes(clientId));
}

function normalizeAppOrigin(value: unknown) {
  const normalized = String(value ?? '').trim();

  if (!normalized) {
    return '';
  }

  try {
    const url = new URL(normalized);
    return url.origin;
  } catch {
    return '';
  }
}

function normalizeAppDomain(value: unknown) {
  return String(value ?? '').trim().toLowerCase();
}

function resolveAppDomainFromOrigin(origin: string) {
  if (!origin) {
    return '';
  }

  try {
    return new URL(origin).hostname.trim().toLowerCase();
  } catch {
    return '';
  }
}

function isAllowedAppTarget(
  item: {
    appOrigin?: string;
    appDomain?: string;
  },
  targetAppOrigins: string[],
  targetAppDomains: string[],
) {
  if (targetAppOrigins.length === 0 && targetAppDomains.length === 0) {
    return true;
  }

  if (targetAppOrigins.length > 0) {
    if (!item.appOrigin || !targetAppOrigins.includes(item.appOrigin)) {
      return false;
    }
  }

  if (targetAppDomains.length > 0) {
    if (!item.appDomain || !targetAppDomains.includes(item.appDomain)) {
      return false;
    }
  }

  return true;
}

type WebPushFailureDetail = {
  endpoint: string;
  statusCode?: number;
  detail?: string;
  code?: string;
  attemptCount: number;
};

function buildScopedPwaNotificationTargetId(token: string, clientId: string) {
  return [token.trim(), clientId.trim()].filter(Boolean).join('::client::');
}

let providerPromise: Promise<Provider | null> | null = null;
let providerSignature: string | null = null;

function normalizePrivateKey(privateKey: string) {
  return privateKey.replace(/\\n/g, '\n');
}

async function loadPrivateKey(env: ReturnType<typeof getEnv>) {
  if (env.APNS_PRIVATE_KEY?.trim()) {
    return normalizePrivateKey(env.APNS_PRIVATE_KEY.trim());
  }

  if (env.APNS_PRIVATE_KEY_PATH?.trim()) {
    const file = await readFile(env.APNS_PRIVATE_KEY_PATH.trim(), 'utf8');
    return file.trim();
  }

  return null;
}

function hasWebPushConfig(env: ReturnType<typeof getEnv>) {
  return Boolean(
    env.WEB_PUSH_ENABLED &&
      env.WEB_PUSH_VAPID_PUBLIC_KEY?.trim() &&
      env.WEB_PUSH_VAPID_PRIVATE_KEY?.trim() &&
      env.WEB_PUSH_SUBJECT?.trim(),
  );
}

function ensureWebPushConfigured(env: ReturnType<typeof getEnv>) {
  if (!hasWebPushConfig(env)) {
    return false;
  }

  webpush.setVapidDetails(
    env.WEB_PUSH_SUBJECT,
    env.WEB_PUSH_VAPID_PUBLIC_KEY!.trim(),
    env.WEB_PUSH_VAPID_PRIVATE_KEY!.trim(),
  );

  return true;
}

function normalizeNotificationDetailText(text?: string | null) {
  const normalized = String(text ?? '').trim();
  return normalized || undefined;
}

export function withInferredNotificationOriginData(payload: IosNotificationPayload): IosNotificationPayload {
  const targetAppOrigin = normalizeTargetAppOrigins(payload.targetAppOrigins)[0] ?? '';
  const targetAppDomain =
    normalizeTargetAppDomains(payload.targetAppDomains)[0] ?? resolveAppDomainFromOrigin(targetAppOrigin);
  const currentData = payload.data ?? {};
  const currentAppOrigin = normalizeAppOrigin(currentData.appOrigin);
  const currentAppDomain = normalizeAppDomain(currentData.appDomain);

  if (
    (!targetAppOrigin || currentAppOrigin === targetAppOrigin) &&
    (!targetAppDomain || currentAppDomain === targetAppDomain)
  ) {
    return payload;
  }

  return {
    ...payload,
    data: {
      ...currentData,
      ...(currentAppOrigin ? {} : targetAppOrigin ? { appOrigin: targetAppOrigin } : {}),
      ...(currentAppDomain ? {} : targetAppDomain ? { appDomain: targetAppDomain } : {}),
    },
  };
}

function isChatNotificationPayload(payload: IosNotificationPayload) {
  const category = String(payload.data?.category ?? '').trim().toLowerCase();
  const threadId = String(payload.threadId ?? '').trim().toLowerCase();
  return category === 'chat' || threadId.startsWith('chat:');
}

function isRetryableWebPushError(error: any) {
  const statusCode = Number(error?.statusCode ?? 0);

  if ([408, 425, 429, 500, 502, 503, 504].includes(statusCode)) {
    return true;
  }

  const code = String(error?.code ?? '').trim().toUpperCase();
  return ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'EAI_AGAIN', 'UND_ERR_CONNECT_TIMEOUT'].includes(code);
}

async function waitForRetry(delayMs: number) {
  await new Promise((resolve) => setTimeout(resolve, delayMs));
}

async function sendWebPushWithRetry(subscription: WebPushSubscriptionPayload, payloadText: string) {
  let lastError: unknown;

  for (let attempt = 1; attempt <= 2; attempt += 1) {
    try {
      await webpush.sendNotification(subscription, payloadText);
      return { attemptCount: attempt };
    } catch (error) {
      lastError = error;

      if (attempt >= 2 || !isRetryableWebPushError(error)) {
        throw {
          error,
          attemptCount: attempt,
        };
      }

      await waitForRetry(250 * attempt);
    }
  }

  throw {
    error: lastError,
    attemptCount: 2,
  };
}

function buildProviderSignature(env: ReturnType<typeof getEnv>) {
  return [
    env.IOS_NOTIFICATION_ENABLED,
    env.APNS_KEY_ID,
    env.APNS_TEAM_ID,
    env.APNS_BUNDLE_ID,
    env.APNS_PRIVATE_KEY,
    env.APNS_PRIVATE_KEY_PATH,
    env.APNS_PRODUCTION,
  ].join('|');
}

async function createProvider(env: ReturnType<typeof getEnv>) {
  if (
    !env.IOS_NOTIFICATION_ENABLED ||
    !env.APNS_KEY_ID ||
    !env.APNS_TEAM_ID ||
    !env.APNS_BUNDLE_ID
  ) {
    return null;
  }

  const privateKey = await loadPrivateKey(env);

  if (!privateKey) {
    return null;
  }

  return new Provider({
    token: {
      key: privateKey,
      keyId: env.APNS_KEY_ID,
      teamId: env.APNS_TEAM_ID,
    },
    production: env.APNS_PRODUCTION,
  });
}

async function getProvider() {
  const env = getEnv();
  const signature = buildProviderSignature(env);

  if (signature !== providerSignature) {
    providerSignature = signature;
    providerPromise = null;
  }

  if (!providerPromise) {
    providerPromise = createProvider(env);
  }

  return providerPromise;
}

async function ensureNotificationTokenTable() {
  const hasTable = await db.schema.hasTable(NOTIFICATION_TOKEN_TABLE);

  if (!hasTable) {
    await db.schema.createTable(NOTIFICATION_TOKEN_TABLE, (table) => {
      table.increments('id').primary();
      table.string('platform', 20).notNullable().defaultTo('ios');
      table.string('device_token', 255).notNullable().unique();
      table.string('device_id', 200).nullable();
      table.string('app_origin', 500).nullable();
      table.string('app_domain', 255).nullable();
      table.boolean('is_enabled').notNullable().defaultTo(true);
      table.timestamp('last_registered_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
      table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
      table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
    });

    return;
  }

  const requiredColumns: Array<[string, (table: any) => void]> = [
    ['platform', (table) => table.string('platform', 20).notNullable().defaultTo('ios')],
    ['device_token', (table) => table.string('device_token', 255).notNullable()],
    ['device_id', (table) => table.string('device_id', 200).nullable()],
    ['app_origin', (table) => table.string('app_origin', 500).nullable()],
    ['app_domain', (table) => table.string('app_domain', 255).nullable()],
    ['is_enabled', (table) => table.boolean('is_enabled').notNullable().defaultTo(true)],
    [
      'last_registered_at',
      (table) => table.timestamp('last_registered_at', { useTz: true }).notNullable().defaultTo(db.fn.now()),
    ],
    ['created_at', (table) => table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now())],
    ['updated_at', (table) => table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now())],
  ];

  for (const [columnName, createColumn] of requiredColumns) {
    const hasColumn = await db.schema.hasColumn(NOTIFICATION_TOKEN_TABLE, columnName);

    if (!hasColumn) {
      await db.schema.alterTable(NOTIFICATION_TOKEN_TABLE, (table) => {
        createColumn(table);
      });
    }
  }
}

async function ensureWebPushSubscriptionTable() {
  const hasTable = await db.schema.hasTable(WEB_PUSH_SUBSCRIPTION_TABLE);

  if (!hasTable) {
    await db.schema.createTable(WEB_PUSH_SUBSCRIPTION_TABLE, (table) => {
      table.increments('id').primary();
      table.string('endpoint', 1000).notNullable().unique();
      table.jsonb('subscription_json').notNullable();
      table.string('device_id', 200).nullable();
      table.string('client_id', 200).nullable();
      table.text('user_agent').nullable();
      table.string('app_origin', 500).nullable();
      table.string('app_domain', 255).nullable();
      table.boolean('is_enabled').notNullable().defaultTo(true);
      table.timestamp('last_registered_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
      table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
      table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
    });

    return;
  }

  const requiredColumns: Array<[string, (table: any) => void]> = [
    ['endpoint', (table) => table.string('endpoint', 1000).notNullable()],
    ['subscription_json', (table) => table.jsonb('subscription_json').notNullable().defaultTo('{}')],
    ['device_id', (table) => table.string('device_id', 200).nullable()],
    ['client_id', (table) => table.string('client_id', 200).nullable()],
    ['user_agent', (table) => table.text('user_agent').nullable()],
    ['app_origin', (table) => table.string('app_origin', 500).nullable()],
    ['app_domain', (table) => table.string('app_domain', 255).nullable()],
    ['is_enabled', (table) => table.boolean('is_enabled').notNullable().defaultTo(true)],
    [
      'last_registered_at',
      (table) => table.timestamp('last_registered_at', { useTz: true }).notNullable().defaultTo(db.fn.now()),
    ],
    ['created_at', (table) => table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now())],
    ['updated_at', (table) => table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now())],
  ];

  for (const [columnName, createColumn] of requiredColumns) {
    const hasColumn = await db.schema.hasColumn(WEB_PUSH_SUBSCRIPTION_TABLE, columnName);

    if (!hasColumn) {
      await db.schema.alterTable(WEB_PUSH_SUBSCRIPTION_TABLE, (table) => {
        createColumn(table);
      });
    }
  }
}

async function ensureNotificationPreferenceTable() {
  const hasTable = await db.schema.hasTable(NOTIFICATION_PREFERENCE_TABLE);

  if (!hasTable) {
    await db.schema.createTable(NOTIFICATION_PREFERENCE_TABLE, (table) => {
      table.increments('id').primary();
      table.string('target_kind', 40).notNullable();
      table.string('target_id', 1000).notNullable();
      table.jsonb('config_json').notNullable().defaultTo('{}');
      table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
      table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
      table.unique(['target_kind', 'target_id']);
    });

    return;
  }

  const requiredColumns: Array<[string, (table: any) => void]> = [
    ['target_kind', (table) => table.string('target_kind', 40).notNullable().defaultTo('client')],
    ['target_id', (table) => table.string('target_id', 1000).notNullable().defaultTo('')],
    ['config_json', (table) => table.jsonb('config_json').notNullable().defaultTo('{}')],
    ['created_at', (table) => table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now())],
    ['updated_at', (table) => table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now())],
  ];

  for (const [columnName, createColumn] of requiredColumns) {
    const hasColumn = await db.schema.hasColumn(NOTIFICATION_PREFERENCE_TABLE, columnName);

    if (!hasColumn) {
      await db.schema.alterTable(NOTIFICATION_PREFERENCE_TABLE, (table) => {
        createColumn(table);
      });
    }
  }
}

export async function setupNotificationTables() {
  await ensureNotificationTokenTable();
  await ensureWebPushSubscriptionTable();
  await ensureNotificationPreferenceTable();
  await ensureNotificationMessagesTable();

  return {
    ok: true,
    tables: [NOTIFICATION_TOKEN_TABLE, WEB_PUSH_SUBSCRIPTION_TABLE, NOTIFICATION_PREFERENCE_TABLE, 'notification_messages'],
  };
}

export function getWebPushConfig() {
  const env = getEnv();
  return {
    enabled: hasWebPushConfig(env),
    publicKey: hasWebPushConfig(env) ? env.WEB_PUSH_VAPID_PUBLIC_KEY!.trim() : '',
  };
}

export async function listIosNotificationTokens() {
  await ensureNotificationTokenTable();

  const rows = await db(NOTIFICATION_TOKEN_TABLE)
    .where({ platform: 'ios' })
    .orderBy('updated_at', 'desc');

  return rows.map((row) => ({
    id: row.id,
    platform: row.platform,
    token: row.device_token,
    deviceId: row.device_id,
    appOrigin: row.app_origin ? String(row.app_origin) : '',
    appDomain: row.app_domain ? String(row.app_domain) : '',
    enabled: row.is_enabled,
    lastRegisteredAt: row.last_registered_at,
    createdAt: row.created_at,
    updatedAt: row.updated_at,
  }));
}

export async function listWebPushSubscriptions() {
  await ensureWebPushSubscriptionTable();

  const rows = await db(WEB_PUSH_SUBSCRIPTION_TABLE).orderBy('updated_at', 'desc');

  return rows.map((row) => ({
    id: row.id,
    endpoint: String(row.endpoint ?? ''),
    deviceId: row.device_id ? String(row.device_id) : '',
    clientId: row.client_id ? String(row.client_id) : '',
    userAgent: row.user_agent ? String(row.user_agent) : '',
    appOrigin: row.app_origin ? String(row.app_origin) : '',
    appDomain: row.app_domain ? String(row.app_domain) : '',
    enabled: Boolean(row.is_enabled),
    lastRegisteredAt: row.last_registered_at,
    createdAt: row.created_at,
    updatedAt: row.updated_at,
  }));
}

export async function registerIosNotificationToken(payload: z.infer<typeof registerIosTokenSchema>) {
  await ensureNotificationTokenTable();
  const appOrigin = normalizeAppOrigin(payload.appOrigin);
  const appDomain = normalizeAppDomain(payload.appDomain) || resolveAppDomainFromOrigin(appOrigin);

  if (!payload.enabled) {
    await unregisterIosNotificationToken(payload.token);

    return {
      ok: true,
      removed: true,
      token: payload.token,
    };
  }

  await db(NOTIFICATION_TOKEN_TABLE)
    .insert({
      platform: 'ios',
      device_token: payload.token,
      device_id: payload.deviceId ?? null,
      app_origin: appOrigin || null,
      app_domain: appDomain || null,
      is_enabled: true,
      last_registered_at: db.fn.now(),
      updated_at: db.fn.now(),
    })
    .onConflict('device_token')
    .merge({
      platform: 'ios',
      device_id: payload.deviceId ?? null,
      app_origin: appOrigin || null,
      app_domain: appDomain || null,
      is_enabled: true,
      last_registered_at: db.fn.now(),
      updated_at: db.fn.now(),
    });

  return {
    ok: true,
    token: payload.token,
  };
}

function parseAutomationNotificationPreference(raw: unknown): AutomationNotificationPreference {
  if (typeof raw === 'string') {
    try {
      return automationNotificationPreferenceSchema.parse(JSON.parse(raw));
    } catch {
      return {};
    }
  }

  return automationNotificationPreferenceSchema.parse(raw ?? {});
}

export async function getAutomationNotificationPreference(
  targetId: string,
  targetKind: NotificationTargetKind = 'client',
) {
  const normalizedTargetId = targetId.trim();

  if (!normalizedTargetId) {
    return null;
  }

  await ensureNotificationPreferenceTable();

  const row = await db(NOTIFICATION_PREFERENCE_TABLE)
    .where({
      target_kind: targetKind,
      target_id: normalizedTargetId,
    })
    .first();

  if (!row) {
    return null;
  }

  return parseAutomationNotificationPreference(row.config_json);
}

export async function upsertAutomationNotificationPreference(
  payload: z.infer<typeof registerAutomationNotificationPreferenceSchema> & { targetId: string },
) {
  const targetId = payload.targetId.trim();
  await ensureNotificationPreferenceTable();

  await db(NOTIFICATION_PREFERENCE_TABLE)
    .insert({
      target_kind: payload.targetKind,
      target_id: targetId,
      config_json: payload.automation,
      updated_at: db.fn.now(),
    })
    .onConflict(['target_kind', 'target_id'])
    .merge({
      config_json: payload.automation,
      updated_at: db.fn.now(),
    });

  return {
    ok: true,
    targetKind: payload.targetKind,
    targetId,
    automation: payload.automation,
  };
}

export async function unregisterIosNotificationToken(token: string) {
  await ensureNotificationTokenTable();

  const deletedCount = await db(NOTIFICATION_TOKEN_TABLE)
    .where({
      platform: 'ios',
      device_token: token,
    })
    .delete();

  return {
    ok: true,
    removed: deletedCount > 0,
    token,
  };
}

export async function registerWebPushSubscription(
  payload: z.infer<typeof registerWebPushSubscriptionSchema>,
) {
  await ensureWebPushSubscriptionTable();
  const appOrigin = normalizeAppOrigin(payload.appOrigin);
  const appDomain = normalizeAppDomain(payload.appDomain) || resolveAppDomainFromOrigin(appOrigin);
  const cleanupTargetIds = normalizeRegistrationCleanupIds(payload.deviceId, payload.clientId);

  if (!payload.enabled) {
    await unregisterWebPushSubscription(payload.subscription.endpoint);

    return {
      ok: true,
      removed: true,
      endpoint: payload.subscription.endpoint,
    };
  }

  await db(WEB_PUSH_SUBSCRIPTION_TABLE)
    .insert({
      endpoint: payload.subscription.endpoint,
      subscription_json: payload.subscription,
      device_id: payload.deviceId ?? null,
      client_id: payload.clientId ?? null,
      user_agent: payload.userAgent ?? null,
      app_origin: appOrigin || null,
      app_domain: appDomain || null,
      is_enabled: true,
      last_registered_at: db.fn.now(),
      updated_at: db.fn.now(),
    })
    .onConflict('endpoint')
    .merge({
      subscription_json: payload.subscription,
      device_id: payload.deviceId ?? null,
      client_id: payload.clientId ?? null,
      user_agent: payload.userAgent ?? null,
      app_origin: appOrigin || null,
      app_domain: appDomain || null,
      is_enabled: true,
      last_registered_at: db.fn.now(),
      updated_at: db.fn.now(),
    });

  if (cleanupTargetIds.length > 0) {
    await db(WEB_PUSH_SUBSCRIPTION_TABLE)
      .whereNot({ endpoint: payload.subscription.endpoint })
      .andWhere((builder) => {
        builder.whereIn('device_id', cleanupTargetIds).orWhereIn('client_id', cleanupTargetIds);
      })
      .delete();
  }

  await removeLegacyWebPushSubscriptionsForRegistration({
    endpoint: payload.subscription.endpoint,
    deviceId: payload.deviceId,
    clientId: payload.clientId,
    userAgent: payload.userAgent,
    appOrigin,
  });

  return {
    ok: true,
    endpoint: payload.subscription.endpoint,
  };
}

export async function unregisterWebPushSubscription(endpoint: string) {
  await ensureWebPushSubscriptionTable();

  const deletedCount = await db(WEB_PUSH_SUBSCRIPTION_TABLE).where({ endpoint }).delete();

  return {
    ok: true,
    removed: deletedCount > 0,
    endpoint,
  };
}

async function getEnabledIosTokens() {
  await ensureNotificationTokenTable();

  const rows = await db(NOTIFICATION_TOKEN_TABLE)
    .where({
      platform: 'ios',
      is_enabled: true,
    })
    .select('device_token', 'device_id', 'app_origin', 'app_domain');

  return rows.map((row) => ({
    token: String(row.device_token),
    deviceId: row.device_id ? String(row.device_id) : '',
    appOrigin: row.app_origin ? String(row.app_origin) : '',
    appDomain: row.app_domain ? String(row.app_domain) : '',
  }));
}

async function getEnabledWebPushSubscriptions() {
  await ensureWebPushSubscriptionTable();

  const rows = await db(WEB_PUSH_SUBSCRIPTION_TABLE)
    .where({
      is_enabled: true,
    })
    .select('endpoint', 'subscription_json', 'device_id', 'client_id', 'app_origin', 'app_domain');

  return rows.map((row) => ({
    endpoint: String(row.endpoint),
    subscription: row.subscription_json as WebPushSubscriptionPayload,
    deviceId: row.device_id ? String(row.device_id) : '',
    clientId: row.client_id ? String(row.client_id) : '',
    appOrigin: row.app_origin ? String(row.app_origin) : '',
    appDomain: row.app_domain ? String(row.app_domain) : '',
  }));
}

async function removeInvalidIosTokens(tokens: string[]) {
  if (!tokens.length) {
    return;
  }

  await db(NOTIFICATION_TOKEN_TABLE)
    .whereIn('device_token', tokens)
    .delete();
}

async function removeInvalidWebPushSubscriptions(endpoints: string[]) {
  if (!endpoints.length) {
    return;
  }

  await db(WEB_PUSH_SUBSCRIPTION_TABLE)
    .whereIn('endpoint', endpoints)
    .delete();
}

function shouldNotifyAutomationEvent(
  automation: AutomationNotificationPreference | null | undefined,
  eventType: string,
) {
  if (eventType === 'work-started') {
    return automation?.notifyOnAutomationStart ?? true;
  }

  if (eventType === 'work-progress') {
    return automation?.notifyOnAutomationProgress ?? true;
  }

  if (
    eventType === 'work-completed' ||
    eventType === 'work-noop-complete' ||
    eventType === 'development-completed' ||
    eventType === 'plan-completed'
  ) {
    return automation?.notifyOnAutomationCompletion ?? true;
  }

  if (eventType === 'release-merged') {
    return automation?.notifyOnAutomationRelease ?? true;
  }

  if (eventType === 'main-merged') {
    return automation?.notifyOnAutomationMain ?? true;
  }

  if (
    eventType === 'branch-failed' ||
    eventType === 'work-failed' ||
    eventType === 'release-failed' ||
    eventType === 'main-failed'
  ) {
    return automation?.notifyOnAutomationFailure ?? true;
  }

  if (eventType === 'plan-restarted') {
    return automation?.notifyOnAutomationRestart ?? true;
  }

  if (eventType === 'issue-resolved') {
    return automation?.notifyOnAutomationIssueResolved ?? true;
  }

  return true;
}

async function isNotificationRecipientAllowed(
  preferenceTargets: NotificationPreferenceTarget[],
  payload: IosNotificationPayload,
) {
  const eventType = payload.data.eventType?.trim();

  if (!eventType) {
    return true;
  }

  for (const target of preferenceTargets) {
    if (!target.id.trim()) {
      continue;
    }

    const automation = await getAutomationNotificationPreference(target.id, target.kind);

    if (automation) {
      return shouldNotifyAutomationEvent(automation, eventType);
    }
  }

  return true;
}

export async function sendIosNotifications(payload: IosNotificationPayload) {
  const env = getEnv();
  const provider = await getProvider();
  const targetDeviceIds = normalizeTargetDeviceIds(payload);
  const targetClientIds = normalizeTargetClientIds(payload);
  const targetAppOrigins = normalizeTargetAppOrigins(payload.targetAppOrigins);
  const targetAppDomains = normalizeTargetAppDomains(payload.targetAppDomains);

  if (!provider || !env.APNS_BUNDLE_ID) {
    return {
      ok: false,
      skipped: true,
      reason: 'APNs 설정이 비어 있습니다.',
      sentCount: 0,
      failedCount: 0,
    };
  }

  const tokenRows = await getEnabledIosTokens();
  const tokens = (
    await Promise.all(
      tokenRows.map(async (row) => ({
        token: row.token,
        deviceId: row.deviceId,
        appOrigin: row.appOrigin,
        appDomain: row.appDomain,
        allowed: await isNotificationRecipientAllowed(
          [
            { kind: 'ios-token-client', id: buildScopedPwaNotificationTargetId(row.token, row.deviceId) },
            { kind: 'ios-token', id: row.token },
            { kind: 'client', id: row.deviceId },
          ],
          payload,
        ),
      })),
    )
  )
    .filter(
      (row) =>
        row.allowed &&
        isAllowedTargetRecipient({ deviceId: row.deviceId }, targetDeviceIds, targetClientIds) &&
        isAllowedAppTarget(row, targetAppOrigins, targetAppDomains),
    )
    .map((row) => row.token);

  if (!tokens.length) {
    return {
      ok: true,
      skipped: true,
      reason: '등록된 iOS 알림 토큰이 없습니다.',
      sentCount: 0,
      failedCount: 0,
    };
  }

  const notification = new Notification();
  notification.topic = env.APNS_BUNDLE_ID;
  notification.pushType = 'alert';
  notification.priority = 10;
  notification.expiry = Math.floor(Date.now() / 1000) + 3600;
  notification.alert = {
    title: payload.title,
    body: payload.body,
  };
  notification.sound = 'default';
  notification.badge = 1;
  notification.payload = payload.data;
  if (payload.threadId) {
    notification.threadId = payload.threadId;
  }

  const response = await provider.send(notification, tokens);
  const invalidTokens = response.failed
    .map((result) => result.device)
    .filter((device): device is string => Boolean(device));

  await removeInvalidIosTokens(invalidTokens);

  return {
    ok: response.failed.length === 0,
    skipped: false,
    sentCount: response.sent.length,
    failedCount: response.failed.length,
    invalidTokens,
  };
}

async function sendWebPushNotifications(payload: IosNotificationPayload) {
  const env = getEnv();
  const targetDeviceIds = normalizeTargetDeviceIds(payload);
  const targetClientIds = normalizeTargetClientIds(payload);
  const targetAppOrigins = normalizeTargetAppOrigins(payload.targetAppOrigins);
  const targetAppDomains = normalizeTargetAppDomains(payload.targetAppDomains);
  if (!ensureWebPushConfigured(env)) {
    return {
      ok: false,
      skipped: true,
      reason: 'Web Push 설정이 비어 있습니다.',
      sentCount: 0,
      failedCount: 0,
    };
  }

  const subscriptions = (
    await Promise.all(
      (await getEnabledWebPushSubscriptions()).map(async (row) => ({
        ...row,
        allowed: await isNotificationRecipientAllowed(
          [
            { kind: 'web-endpoint', id: row.endpoint },
            { kind: 'client', id: row.deviceId },
            { kind: 'client', id: row.clientId },
          ],
          payload,
        ),
      })),
    )
  ).filter(
      (row) =>
      row.allowed &&
      isAllowedTargetRecipient({ deviceId: row.deviceId, clientId: row.clientId }, targetDeviceIds, targetClientIds) &&
      isAllowedAppTarget(row, targetAppOrigins, targetAppDomains),
  );
  const matchedSubscriptions = subscriptions.map((row) => ({
    endpoint: row.endpoint,
    deviceId: row.deviceId,
    clientId: row.clientId,
    appOrigin: row.appOrigin,
    appDomain: row.appDomain,
  }));

  if (!subscriptions.length) {
    return {
      ok: true,
      skipped: true,
      reason: '등록된 Web Push 구독이 없습니다.',
      sentCount: 0,
      failedCount: 0,
      matchedCount: 0,
      matchedSubscriptions,
    };
  }

  const payloadText = JSON.stringify({
    title: payload.title,
    body: payload.body,
    data: payload.data,
    threadId: payload.threadId,
  });
  const preserveSubscriptions = isChatNotificationPayload(payload);

  const invalidEndpoints: string[] = [];
  let sentCount = 0;
  let failedCount = 0;
  const failures: WebPushFailureDetail[] = [];

  await Promise.all(
    subscriptions.map(async ({ endpoint, subscription }) => {
      try {
        await sendWebPushWithRetry(subscription, payloadText);
        sentCount += 1;
      } catch (error: any) {
        const deliveryError = error?.error ?? error;
        const attemptCount = Number(error?.attemptCount ?? 1);
        failedCount += 1;
        const statusCode = Number(deliveryError?.statusCode ?? 0);
        const detail =
          normalizeNotificationDetailText(deliveryError?.body) ?? normalizeNotificationDetailText(deliveryError?.message);
        const code = normalizeNotificationDetailText(deliveryError?.code);

        if (!preserveSubscriptions && (statusCode === 404 || statusCode === 410)) {
          invalidEndpoints.push(endpoint);
        }

        failures.push({
          endpoint,
          statusCode: statusCode || undefined,
          detail,
          code,
          attemptCount,
        });
      }
    }),
  );

  if (!preserveSubscriptions) {
    await removeInvalidWebPushSubscriptions(invalidEndpoints);
  }

  if (failures.length) {
    console.warn(
      '[notification-service] web push delivery failed',
      JSON.stringify({
        failedCount,
        preserveSubscriptions,
        invalidEndpointCount: invalidEndpoints.length,
        failures,
      }),
    );
  }

  return {
    ok: failedCount === 0,
    skipped: false,
    sentCount,
    failedCount,
    invalidEndpoints,
    matchedCount: matchedSubscriptions.length,
    matchedSubscriptions,
  };
}

export async function sendNotifications(
  payload: IosNotificationPayload,
  options?: {
    disableIos?: boolean;
    disableWebPush?: boolean;
  },
) {
  const resolvedPayload = withInferredNotificationOriginData(payload);
  const [ios, web] = await Promise.all([
    options?.disableIos
      ? Promise.resolve({
          ok: true,
          skipped: true,
          reason: '요청 설정에 따라 iOS 알림 발송을 건너뛰었습니다.',
          sentCount: 0,
          failedCount: 0,
          invalidTokens: [],
        })
      : sendIosNotifications(resolvedPayload),
    options?.disableWebPush
      ? Promise.resolve({
          ok: true,
          skipped: true,
          reason: '요청 설정에 따라 Web Push 발송을 건너뛰었습니다.',
          sentCount: 0,
          failedCount: 0,
          invalidEndpoints: [],
        })
      : sendWebPushNotifications(resolvedPayload),
  ]);

  const aggregate = resolveNotificationAggregateResult(
    {
      ios,
      web,
    },
    options,
  );

  return {
    ok: aggregate.ok,
    skipped: aggregate.skipped,
    ios,
    web,
  };
}

export function resolveNotificationAggregateResult(
  result: {
    ios: { ok: boolean; skipped: boolean };
    web: { ok: boolean; skipped: boolean };
  },
  options?: {
    disableIos?: boolean;
    disableWebPush?: boolean;
  },
) {
  const enabledResults = [
    options?.disableIos ? null : result.ios,
    options?.disableWebPush ? null : result.web,
  ].filter((entry): entry is { ok: boolean; skipped: boolean } => Boolean(entry));

  return {
    ok: enabledResults.length === 0 ? true : enabledResults.every((entry) => entry.ok),
    skipped: enabledResults.length === 0 ? true : enabledResults.every((entry) => entry.skipped),
  };
}

export async function shutdownNotificationProvider() {
  const provider = await getProvider();
  provider?.shutdown();
  providerPromise = null;
}
