import i18n from "i18next";
import { ReactNode, isValidElement } from "react";

type NoticeType = "success" | "error" | "info";

export interface NoticeTranslationDescriptor {
  key: string;
  params?: Record<string, unknown>;
}

interface NoticeItem {
  readonly id: number;
  readonly type: NoticeType;
  readonly duration: number;
  readonly message?: ReactNode;
  readonly i18n?: NoticeTranslationDescriptor;
  timerId?: ReturnType<typeof setTimeout>;
}

type NoticeContent = unknown;

type NoticeExtra = unknown;

type NoticeShortcut = (
  message: NoticeContent,
  ...extras: NoticeExtra[]
) => number;

type ShowNotice = ((
  type: NoticeType,
  message: NoticeContent,
  ...extras: NoticeExtra[]
) => number) & {
  success: NoticeShortcut;
  error: NoticeShortcut;
  info: NoticeShortcut;
};

type NoticeSubscriber = () => void;

const DEFAULT_DURATIONS: Readonly<Record<NoticeType, number>> = {
  success: 3000,
  info: 5000,
  error: 8000,
};

const TRANSLATION_KEY_PATTERN = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+$/;

let nextId = 0;
let notices: NoticeItem[] = [];
const subscribers: Set<NoticeSubscriber> = new Set();

function notifySubscribers() {
  subscribers.forEach((subscriber) => subscriber());
}

interface ParsedNoticeExtras {
  params?: Record<string, unknown>;
  raw?: unknown;
  duration?: number;
}

function parseNoticeExtras(extras: NoticeExtra[]): ParsedNoticeExtras {
  let params: Record<string, unknown> | undefined;
  let raw: unknown;
  let duration: number | undefined;

  // Prioritize objects as translation params, then as raw payloads, while the first number wins as duration.
  for (const extra of extras) {
    if (extra === undefined) continue;

    if (typeof extra === "number" && duration === undefined) {
      duration = extra;
      continue;
    }

    if (isPlainRecord(extra)) {
      if (!params) {
        params = extra;
        continue;
      }
      if (!raw) {
        raw = extra;
        continue;
      }
    }

    if (!raw) {
      raw = extra;
      continue;
    }

    if (!params && isPlainRecord(extra)) {
      params = extra;
      continue;
    }

    if (duration === undefined && typeof extra === "number") {
      duration = extra;
    }
  }

  return { params, raw, duration };
}

function resolveDuration(type: NoticeType, override?: number) {
  return override ?? DEFAULT_DURATIONS[type];
}

function buildNotice(
  id: number,
  type: NoticeType,
  duration: number,
  payload: { message?: ReactNode; i18n?: NoticeTranslationDescriptor },
  timerId?: ReturnType<typeof setTimeout>,
): NoticeItem {
  return {
    id,
    type,
    duration,
    timerId,
    ...payload,
  };
}

function isMaybeTranslationDescriptor(
  message: unknown,
): message is NoticeTranslationDescriptor {
  if (
    typeof message === "object" &&
    message !== null &&
    !Array.isArray(message) &&
    !isValidElement(message)
  ) {
    return typeof (message as Record<string, unknown>).key === "string";
  }
  return false;
}

function isPlainRecord(value: unknown): value is Record<string, unknown> {
  if (
    typeof value !== "object" ||
    value === null ||
    Array.isArray(value) ||
    value instanceof Error ||
    isValidElement(value)
  ) {
    return false;
  }

  const proto = Object.getPrototypeOf(value);
  return proto === Object.prototype || proto === null;
}

function createRawDescriptor(message: string): NoticeTranslationDescriptor {
  return {
    key: "shared.feedback.notices.raw",
    params: { message },
  };
}

function isLikelyTranslationKey(key: string) {
  return TRANSLATION_KEY_PATTERN.test(key);
}

function shouldUseTranslationKey(
  key: string,
  params?: Record<string, unknown>,
) {
  if (params && Object.keys(params).length > 0) return true;
  if (isLikelyTranslationKey(key)) return true;
  if (i18n.isInitialized) {
    return i18n.exists(key);
  }
  return false;
}

function extractDisplayText(input: unknown): string | undefined {
  if (input === null || input === undefined) return undefined;
  if (typeof input === "string") return input;
  if (typeof input === "number" || typeof input === "boolean") {
    return String(input);
  }
  if (input instanceof Error) {
    return input.message || input.name;
  }
  if (typeof input === "object" && input !== null) {
    const maybeMessage = (input as { message?: unknown }).message;
    if (typeof maybeMessage === "string") return maybeMessage;
  }
  try {
    return JSON.stringify(input);
  } catch {
    return String(input);
  }
}

function normalizeNoticeMessage(
  message: NoticeContent,
  params?: Record<string, unknown>,
  raw?: unknown,
): { message?: ReactNode; i18n?: NoticeTranslationDescriptor } {
  const rawText = raw !== undefined ? extractDisplayText(raw) : undefined;

  if (isValidElement(message)) {
    return { message };
  }

  if (isMaybeTranslationDescriptor(message)) {
    const originalParams = message.params ?? {};
    const mergedParams = Object.keys(params ?? {}).length
      ? { ...originalParams, ...params }
      : { ...originalParams };

    if (rawText !== undefined) {
      return {
        i18n: {
          key: "shared.feedback.notices.prefixedRaw",
          params: {
            ...mergedParams,
            prefixKey: message.key,
            prefixParams: originalParams,
            message: rawText,
          },
        },
      };
    }

    return {
      i18n: {
        key: message.key,
        params: Object.keys(mergedParams).length ? mergedParams : undefined,
      },
    };
  }

  if (typeof message === "string") {
    if (rawText !== undefined) {
      if (shouldUseTranslationKey(message, params)) {
        return {
          i18n: {
            key: "shared.feedback.notices.prefixedRaw",
            params: {
              ...(params ?? {}),
              prefixKey: message,
              message: rawText,
            },
          },
        };
      }
      // Prefer showing the original string while still surfacing the raw details below.
      return {
        i18n: {
          key: "shared.feedback.notices.prefixedRaw",
          params: {
            ...(params ?? {}),
            prefix: message,
            message: rawText,
          },
        },
      };
    }

    if (shouldUseTranslationKey(message, params)) {
      return {
        i18n: {
          key: message,
          params: params && Object.keys(params).length ? params : undefined,
        },
      };
    }
    return { i18n: createRawDescriptor(message) };
  }

  if (rawText !== undefined) {
    return { i18n: createRawDescriptor(rawText) };
  }

  const extracted = extractDisplayText(message);
  if (extracted !== undefined) {
    return { i18n: createRawDescriptor(extracted) };
  }

  return { i18n: createRawDescriptor("") };
}

const baseShowNotice = (
  type: NoticeType,
  message: NoticeContent,
  ...extras: NoticeExtra[]
): number => {
  const id = nextId++;
  const { params, raw, duration } = parseNoticeExtras(extras);
  const effectiveDuration = resolveDuration(type, duration);
  const timerId =
    effectiveDuration > 0
      ? setTimeout(() => hideNotice(id), effectiveDuration)
      : undefined;

  const normalizedMessage = normalizeNoticeMessage(message, params, raw);
  const notice = buildNotice(
    id,
    type,
    effectiveDuration,
    normalizedMessage,
    timerId,
  );

  notices = [...notices, notice];
  notifySubscribers();
  return id;
};

/**
 * Shows a global notice; `showNotice.success / error / info` are the usual entry points.
 *
 * - `message`: i18n key string, `{ key, params }`, ReactNode, Error/any value (message is extracted)
 * - `extras` parsed left-to-right: first plain object is i18n params; next value is raw payload; first number overrides duration (ms, 0 = persistent; defaults: success 3000 / info 5000 / error 8000)
 * - Returns a notice id for manual closing via `hideNotice(id)`
 *
 * @example showNotice.success("profiles.page.feedback.notifications.batchDeleted");
 * @example showNotice.error(err); // pass an Error directly
 * @example showNotice.error("shared.feedback.notifications.common.refreshFailed", err); // Simply pass an Error directly; but we recommend using { err } with i18n key and placeholders.
 * @example showNotice.error("profiles.page.feedback.errors.invalidUrl", { url }, 4000);
 */
export const showNotice: ShowNotice = Object.assign(baseShowNotice, {
  success: (message: NoticeContent, ...extras: NoticeExtra[]) =>
    baseShowNotice("success", message, ...extras),
  error: (message: NoticeContent, ...extras: NoticeExtra[]) =>
    baseShowNotice("error", message, ...extras),
  info: (message: NoticeContent, ...extras: NoticeExtra[]) =>
    baseShowNotice("info", message, ...extras),
});

export function hideNotice(id: number) {
  const notice = notices.find((candidate) => candidate.id === id);
  if (notice?.timerId) {
    clearTimeout(notice.timerId);
  }
  notices = notices.filter((candidate) => candidate.id !== id);
  notifySubscribers();
}

export function subscribeNotices(subscriber: NoticeSubscriber) {
  subscribers.add(subscriber);
  return () => {
    subscribers.delete(subscriber);
  };
}

export function getSnapshotNotices() {
  return notices;
}
