import { z } from 'zod';
import { db } from '../db/client.js';
import { selectNotificationMessageIdsToDelete } from './notification-message-prune.js';

export const NOTIFICATION_MESSAGE_TABLE = 'notification_messages';

const notificationMessagePrioritySchema = z.enum(['low', 'normal', 'high', 'urgent']);
const notificationMessageListStatusSchema = z.enum(['all', 'unread']);

export const notificationMessageListQuerySchema = z.object({
  status: notificationMessageListStatusSchema.default('all'),
  limit: z.coerce.number().int().min(1).max(100).default(20),
});

export const notificationMessagePayloadSchema = z.object({
  title: z.string().trim().min(1).max(200),
  body: z.string().trim().min(1).max(20000),
  category: z.string().trim().min(1).max(60).default('general'),
  source: z.string().trim().min(1).max(80).default('system'),
  priority: notificationMessagePrioritySchema.default('normal'),
  metadata: z.record(z.string(), z.unknown()).default({}),
});

export const notificationMessageReadPayloadSchema = z.object({
  read: z.boolean().default(true),
});

export type NotificationMessageItem = {
  id: number;
  title: string;
  body: string;
  preview: string;
  category: string;
  source: string;
  priority: z.infer<typeof notificationMessagePrioritySchema>;
  read: boolean;
  readAt: string | null;
  metadata: Record<string, unknown>;
  createdAt: string;
  updatedAt: string;
};

export type NotificationMessageChangeEvent =
  | {
      action: 'created' | 'updated';
      item: NotificationMessageItem;
    }
  | {
      action: 'deleted';
      id: number;
    };

const notificationMessageChangeListeners = new Set<(event: NotificationMessageChangeEvent) => void>();

function emitNotificationMessageChange(event: NotificationMessageChangeEvent) {
  for (const listener of notificationMessageChangeListeners) {
    try {
      listener(event);
    } catch {
      // noop
    }
  }
}

export function subscribeNotificationMessageChanges(listener: (event: NotificationMessageChangeEvent) => void) {
  notificationMessageChangeListeners.add(listener);

  return () => {
    notificationMessageChangeListeners.delete(listener);
  };
}

function normalizeTargetClientIdsFromMetadata(metadata: Record<string, unknown>) {
  const rawTargetClientIds = metadata.targetClientIds;

  if (!Array.isArray(rawTargetClientIds)) {
    return [];
  }

  return rawTargetClientIds
    .map((value) => (typeof value === 'string' ? value.trim() : ''))
    .filter((value) => value.length > 0);
}

export function pruneNotificationMessageTargetClientIds(
  metadata: Record<string, unknown>,
  clientIds: Iterable<string | null | undefined>,
) {
  const removeClientIds = new Set(
    Array.from(clientIds, (value) => String(value ?? '').trim()).filter(Boolean),
  );
  const currentTargetClientIds = normalizeTargetClientIdsFromMetadata(metadata);

  if (!currentTargetClientIds.length || !removeClientIds.size) {
    return {
      changed: false,
      hasTargets: currentTargetClientIds.length > 0,
      targetClientIds: currentTargetClientIds,
      metadata,
    };
  }

  const nextTargetClientIds = currentTargetClientIds.filter((clientId) => !removeClientIds.has(clientId));

  if (nextTargetClientIds.length === currentTargetClientIds.length) {
    return {
      changed: false,
      hasTargets: true,
      targetClientIds: currentTargetClientIds,
      metadata,
    };
  }

  return {
    changed: true,
    hasTargets: nextTargetClientIds.length > 0,
    targetClientIds: nextTargetClientIds,
    metadata: {
      ...metadata,
      targetClientIds: nextTargetClientIds,
    },
  };
}

function matchesNotificationMessageTargetClient(metadata: Record<string, unknown>, clientId: string) {
  const targetClientIds = normalizeTargetClientIdsFromMetadata(metadata);

  if (targetClientIds.length === 0) {
    return true;
  }

  const normalizedClientId = clientId.trim();

  if (!normalizedClientId) {
    return false;
  }

  return targetClientIds.includes(normalizedClientId);
}

function normalizePreviewText(value: string) {
  const normalized = value
    .replace(/```[\s\S]*?```/g, ' ')
    .replace(/!\[[^\]]*\]\([^)]+\)/g, ' ')
    .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1')
    .replace(/[#>*_`~-]/g, ' ')
    .replace(/\s+/g, ' ')
    .trim();

  return normalized.length > 140 ? `${normalized.slice(0, 137).trimEnd()}...` : normalized;
}

function mapNotificationMessageRow(row: Record<string, unknown>): NotificationMessageItem {
  const body = String(row.body ?? '');
  const metadata =
    typeof row.metadata_json === 'object' && row.metadata_json ? (row.metadata_json as Record<string, unknown>) : {};
  const metadataPreview =
    typeof metadata.previewText === 'string'
      ? metadata.previewText
      : typeof metadata.listPreviewText === 'string'
        ? metadata.listPreviewText
        : '';

  return {
    id: Number(row.id ?? 0),
    title: String(row.title ?? ''),
    body,
    preview: normalizePreviewText(metadataPreview || body),
    category: String(row.category ?? 'general'),
    source: String(row.source ?? 'system'),
    priority: notificationMessagePrioritySchema.catch('normal').parse(row.priority),
    read: Boolean(row.is_read),
    readAt: row.read_at === null || row.read_at === undefined ? null : String(row.read_at),
    metadata,
    createdAt: String(row.created_at ?? ''),
    updatedAt: String(row.updated_at ?? ''),
  };
}

function resolveInsertedId(result: unknown): number | null {
  if (typeof result === 'number' && Number.isInteger(result) && result > 0) {
    return result;
  }

  if (Array.isArray(result)) {
    const first = result[0];

    if (typeof first === 'number' && Number.isInteger(first) && first > 0) {
      return first;
    }

    if (first && typeof first === 'object' && 'id' in first) {
      const id = Number((first as { id?: unknown }).id);
      return Number.isInteger(id) && id > 0 ? id : null;
    }
  }

  if (result && typeof result === 'object' && 'id' in result) {
    const id = Number((result as { id?: unknown }).id);
    return Number.isInteger(id) && id > 0 ? id : null;
  }

  return null;
}

function supportsReturning() {
  const clientName = String(db.client.config.client ?? '').toLowerCase();
  return ['pg', 'postgres', 'postgresql', 'sqlite3', 'better-sqlite3', 'oracledb', 'mssql'].includes(clientName);
}

export async function ensureNotificationMessagesTable() {
  const hasTable = await db.schema.hasTable(NOTIFICATION_MESSAGE_TABLE);

  if (!hasTable) {
    await db.schema.createTable(NOTIFICATION_MESSAGE_TABLE, (table) => {
      table.increments('id').primary();
      table.string('title', 200).notNullable();
      table.text('body').notNullable();
      table.string('category', 60).notNullable().defaultTo('general');
      table.string('source', 80).notNullable().defaultTo('system');
      table.string('priority', 20).notNullable().defaultTo('normal');
      table.boolean('is_read').notNullable().defaultTo(false);
      table.timestamp('read_at', { useTz: true }).nullable();
      table.jsonb('metadata_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());
    });
  }

  const requiredColumns: Array<[string, (table: any) => void]> = [
    ['title', (table) => table.string('title', 200).notNullable().defaultTo('알림')],
    ['body', (table) => table.text('body').notNullable().defaultTo('')],
    ['category', (table) => table.string('category', 60).notNullable().defaultTo('general')],
    ['source', (table) => table.string('source', 80).notNullable().defaultTo('system')],
    ['priority', (table) => table.string('priority', 20).notNullable().defaultTo('normal')],
    ['is_read', (table) => table.boolean('is_read').notNullable().defaultTo(false)],
    ['read_at', (table) => table.timestamp('read_at', { useTz: true }).nullable()],
    ['metadata_json', (table) => table.jsonb('metadata_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_MESSAGE_TABLE, columnName);

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

export async function listNotificationMessages(query: z.infer<typeof notificationMessageListQuerySchema>, clientId = '') {
  await ensureNotificationMessagesTable();
  const parsedQuery = notificationMessageListQuerySchema.parse(query);
  const builder = db(NOTIFICATION_MESSAGE_TABLE)
    .select('*')
    .orderBy('is_read', 'asc')
    .orderBy('created_at', 'desc')
    .orderBy('id', 'desc');

  if (parsedQuery.status === 'unread') {
    builder.where({ is_read: false });
  }

  const rows = await builder;
  const filteredItems = rows
    .map((row) => mapNotificationMessageRow(row))
    .filter((item) => matchesNotificationMessageTargetClient(item.metadata, clientId));
  const unreadCount = filteredItems.filter((item) => item.read !== true).length;

  return {
    items: filteredItems.slice(0, parsedQuery.limit),
    unreadCount,
  };
}

export async function getNotificationMessage(id: number) {
  await ensureNotificationMessagesTable();
  const row = await db(NOTIFICATION_MESSAGE_TABLE).where({ id }).first();
  return row ? mapNotificationMessageRow(row) : null;
}

export async function createNotificationMessage(payload: z.infer<typeof notificationMessagePayloadSchema>) {
  await ensureNotificationMessagesTable();
  const parsedPayload = notificationMessagePayloadSchema.parse(payload);
  const insertQuery = db(NOTIFICATION_MESSAGE_TABLE).insert({
    title: parsedPayload.title,
    body: parsedPayload.body,
    category: parsedPayload.category,
    source: parsedPayload.source,
    priority: parsedPayload.priority,
    metadata_json: parsedPayload.metadata,
    is_read: false,
    read_at: null,
    created_at: db.fn.now(),
    updated_at: db.fn.now(),
  });
  const insertResult = supportsReturning() ? await insertQuery.returning('id') : await insertQuery;
  const insertedId = resolveInsertedId(insertResult);

  if (!insertedId) {
    throw new Error('알림 메시지 저장 후 ID를 확인하지 못했습니다.');
  }

  const row = await db(NOTIFICATION_MESSAGE_TABLE).where({ id: insertedId }).first();

  if (!row) {
    throw new Error('저장된 알림 메시지를 다시 불러오지 못했습니다.');
  }

  const item = mapNotificationMessageRow(row);
  emitNotificationMessageChange({
    action: 'created',
    item,
  });
  return item;
}

export async function deleteOlderNotificationMessagesBySource(source: string, keepLatestCount = 1) {
  await ensureNotificationMessagesTable();
  const normalizedSource = source.trim();

  if (!normalizedSource) {
    return 0;
  }

  const rows = await db(NOTIFICATION_MESSAGE_TABLE)
    .select('id', 'created_at')
    .where({ source: normalizedSource });
  const idsToDelete = selectNotificationMessageIdsToDelete(
    rows.map((row) => ({
      id: Number(row.id ?? 0),
      createdAt: row.created_at === null || row.created_at === undefined ? null : String(row.created_at),
    })),
    keepLatestCount,
  ).filter((id) => Number.isInteger(id) && id > 0);

  if (idsToDelete.length === 0) {
    return 0;
  }

  return db(NOTIFICATION_MESSAGE_TABLE).whereIn('id', idsToDelete).del();
}

export async function updateNotificationMessageReadState(
  id: number,
  payload: z.infer<typeof notificationMessageReadPayloadSchema>,
) {
  await ensureNotificationMessagesTable();
  const parsedPayload = notificationMessageReadPayloadSchema.parse(payload);
  const updatedCount = await db(NOTIFICATION_MESSAGE_TABLE)
    .where({ id })
    .update({
      is_read: parsedPayload.read,
      read_at: parsedPayload.read ? db.fn.now() : null,
      updated_at: db.fn.now(),
    });

  if (!updatedCount) {
    return null;
  }

  const row = await db(NOTIFICATION_MESSAGE_TABLE).where({ id }).first();
  if (!row) {
    return null;
  }

  const item = mapNotificationMessageRow(row);
  emitNotificationMessageChange({
    action: 'updated',
    item,
  });
  return item;
}

export async function cleanupNotificationMessagesForStaleTargetClients(args: {
  sessionId: string;
  staleClientIds: Iterable<string | null | undefined>;
}) {
  await ensureNotificationMessagesTable();

  const sessionId = String(args.sessionId ?? '').trim();
  const staleClientIds = Array.from(args.staleClientIds, (value) => String(value ?? '').trim()).filter(Boolean);

  if (!sessionId || staleClientIds.length === 0) {
    return [] as NotificationMessageItem[];
  }

  const rows = await db(NOTIFICATION_MESSAGE_TABLE)
    .select('*')
    .where({
      source: 'codex-live',
      category: 'chat',
      is_read: false,
    });

  const updatedItems: NotificationMessageItem[] = [];

  for (const row of rows) {
    const metadata =
      typeof row.metadata_json === 'object' && row.metadata_json ? (row.metadata_json as Record<string, unknown>) : {};

    if (String(metadata.sessionId ?? '').trim() !== sessionId) {
      continue;
    }

    const pruned = pruneNotificationMessageTargetClientIds(metadata, staleClientIds);

    if (!pruned.changed) {
      continue;
    }

    await db(NOTIFICATION_MESSAGE_TABLE)
      .where({ id: row.id })
      .update({
        metadata_json: pruned.metadata,
        is_read: pruned.hasTargets ? false : true,
        read_at: pruned.hasTargets ? null : db.fn.now(),
        updated_at: db.fn.now(),
      });

    const updatedRow = await db(NOTIFICATION_MESSAGE_TABLE).where({ id: row.id }).first();

    if (!updatedRow) {
      continue;
    }

    const item = mapNotificationMessageRow(updatedRow);
    updatedItems.push(item);
    emitNotificationMessageChange({
      action: 'updated',
      item,
    });
  }

  return updatedItems;
}

export async function deleteNotificationMessage(id: number) {
  await ensureNotificationMessagesTable();
  const deletedCount = await db(NOTIFICATION_MESSAGE_TABLE).where({ id }).del();

  if (deletedCount > 0) {
    emitNotificationMessageChange({
      action: 'deleted',
      id,
    });
  }

  return deletedCount > 0;
}
