import type pg from 'pg';
import { HttpError } from '../../core/http.js';

export type NotificationStatus = 'unread' | 'read' | 'dismissed' | 'archived';
export type NotificationSeverity = 'info' | 'success' | 'warning' | 'error';

export interface UserNotificationRow {
  id: string;
  tenantId: string;
  userId: string;
  notificationType: string;
  status: NotificationStatus;
  severity: NotificationSeverity;
  title: string;
  message: string;
  actionLabel: string | null;
  actionPath: string | null;
  sourceType: string | null;
  sourceId: string | null;
  dedupeKey: string | null;
  metadata: Record<string, unknown>;
  createdBy: string | null;
  readAt: string | null;
  createdAt: string;
  updatedAt: string;
}

export interface CreateUserNotificationInput {
  tenantId: string;
  userId: string;
  notificationType: string;
  severity?: NotificationSeverity;
  title: string;
  message: string;
  actionLabel?: string | null;
  actionPath?: string | null;
  sourceType?: string | null;
  sourceId?: string | null;
  dedupeKey?: string | null;
  metadata?: Record<string, unknown>;
  createdBy?: string | null;
}

export const USER_NOTIFICATION_STATUSES: NotificationStatus[] = ['unread', 'read', 'dismissed', 'archived'];
export const USER_NOTIFICATION_SEVERITIES: NotificationSeverity[] = ['info', 'success', 'warning', 'error'];

function nullableString(value: unknown) {
  return typeof value === 'string' && value.trim() ? value.trim() : null;
}

function safeText(value: unknown, key: string, maxLength: number) {
  const text = nullableString(value);
  if (!text) throw new HttpError(400, `${key} is required`, 'REQUIRED_FIELD');
  if (text.length > maxLength) {
    throw new HttpError(400, `${key} is too long`, 'FIELD_TOO_LONG');
  }
  return text;
}

function safeNullableText(value: unknown, key: string, maxLength: number) {
  const text = nullableString(value);
  if (!text) return null;
  if (text.length > maxLength) {
    throw new HttpError(400, `${key} is too long`, 'FIELD_TOO_LONG');
  }
  return text;
}

function safeToken(value: unknown, key: string, fallback: string | null = null) {
  const text = nullableString(value) || fallback;
  if (!text) return null;
  if (!/^[a-z][a-z0-9_:-]{1,95}$/i.test(text)) {
    throw new HttpError(400, `${key} contains unsupported characters`, 'INVALID_NOTIFICATION_TOKEN');
  }
  return text;
}

function safeDedupeKey(value: unknown) {
  const text = nullableString(value);
  if (!text) return null;
  if (!/^[a-zA-Z0-9:_./-]{4,180}$/.test(text)) {
    throw new HttpError(400, 'dedupeKey contains unsupported characters', 'INVALID_NOTIFICATION_DEDUPE_KEY');
  }
  return text;
}

function safeUuid(value: unknown, key: string) {
  const text = nullableString(value);
  if (!text) return null;
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(text)) {
    throw new HttpError(400, `${key} must be a UUID`, 'INVALID_UUID');
  }
  return text;
}

function objectValue(value: unknown): Record<string, unknown> {
  return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
}

export function normalizeNotificationStatus(value: unknown, fallback: NotificationStatus = 'unread') {
  const status = (nullableString(value) || fallback) as NotificationStatus;
  if (!USER_NOTIFICATION_STATUSES.includes(status)) {
    throw new HttpError(400, `Invalid notification status: ${status}`, 'INVALID_NOTIFICATION_STATUS');
  }
  return status;
}

export function normalizeNotificationSeverity(value: unknown, fallback: NotificationSeverity = 'info') {
  const severity = (nullableString(value) || fallback) as NotificationSeverity;
  if (!USER_NOTIFICATION_SEVERITIES.includes(severity)) {
    throw new HttpError(400, `Invalid notification severity: ${severity}`, 'INVALID_NOTIFICATION_SEVERITY');
  }
  return severity;
}

export async function createUserNotification(
  client: Pick<pg.PoolClient, 'query'>,
  input: CreateUserNotificationInput,
): Promise<UserNotificationRow | null> {
  const notificationType = safeToken(input.notificationType, 'notificationType');
  const severity = normalizeNotificationSeverity(input.severity, 'info');
  const title = safeText(input.title, 'title', 120);
  const message = safeText(input.message, 'message', 600);
  const actionLabel = safeNullableText(input.actionLabel, 'actionLabel', 40);
  const actionPath = safeNullableText(input.actionPath, 'actionPath', 240);
  const sourceType = safeToken(input.sourceType, 'sourceType', null);
  const sourceId = safeUuid(input.sourceId, 'sourceId');
  const dedupeKey = safeDedupeKey(input.dedupeKey);

  const result = await client.query<UserNotificationRow>(
    `
    insert into public.user_notifications (
      tenant_id, user_id, notification_type, status, severity,
      title, message, action_label, action_path,
      source_type, source_id, dedupe_key, metadata, created_by
    )
    values (
      $1, $2, $3, 'unread', $4,
      $5, $6, $7, $8,
      $9, $10::uuid, $11, $12::jsonb, $13::uuid
    )
    on conflict (tenant_id, user_id, notification_type, dedupe_key)
    where dedupe_key is not null
    do update set severity = excluded.severity,
                  title = excluded.title,
                  message = excluded.message,
                  action_label = excluded.action_label,
                  action_path = excluded.action_path,
                  source_type = excluded.source_type,
                  source_id = excluded.source_id,
                  metadata = public.user_notifications.metadata || excluded.metadata,
                  created_by = coalesce(public.user_notifications.created_by, excluded.created_by),
                  updated_at = now()
    returning id, tenant_id as "tenantId", user_id as "userId",
              notification_type as "notificationType", status, severity,
              title, message, action_label as "actionLabel",
              action_path as "actionPath", source_type as "sourceType",
              source_id as "sourceId", dedupe_key as "dedupeKey",
              metadata, created_by as "createdBy", read_at as "readAt",
              created_at as "createdAt", updated_at as "updatedAt"
    `,
    [
      input.tenantId,
      input.userId,
      notificationType,
      severity,
      title,
      message,
      actionLabel,
      actionPath,
      sourceType,
      sourceId,
      dedupeKey,
      JSON.stringify(objectValue(input.metadata)),
      safeUuid(input.createdBy, 'createdBy'),
    ],
  );

  return result.rows[0] || null;
}
