"use client";

import { z } from "zod";

import { StorageKeys } from "@/data/storage/storage_keys";
import { ChatLockTypeSchema } from "@/data/schemas/chat";
import { getCharacterById } from "@/data/constants/character";
import { normalizeLegacyCharacterId } from "@/data/constants/character_id_aliases";
import { Result } from "@/utils/result";
import { SessionAsyncUtil } from "@/utils/session-storage";

const MAX_AGE_MS = 30 * 60 * 1000;

export type PendingChatUnlockKind = "private" | "voice" | "image";
export type PendingChatUnlockStage = "auth" | "payment";
export type PendingChatPromotionType = "voice" | "image" | "private";

export const PendingChatPromotionSchema = z.object({
  characterId: z.string().min(1),
  promotionType: z.enum(["voice", "image", "private"]),
  lockType: ChatLockTypeSchema,
  clientLockId: z.string().min(1),
  createdAt: z.number(),
});

const PendingChatUnlockSchema = z
  .object({
    reason: z.literal("single_message_unlock"),
    characterId: z.string().min(1),
    displayMessageId: z.string().min(1),
    messageId: z.string().min(1).optional(),
    kind: z.enum(["private", "voice", "image"]),
    lockType: ChatLockTypeSchema.optional(),
    clientLockId: z.string().min(1).optional(),
    promotion: PendingChatPromotionSchema.optional(),
    returnUrl: z
      .string()
      .min(1)
      .refine(
        (value) => value.startsWith("/") && !value.startsWith("//"),
        "returnUrl must be an internal route",
      ),
    stage: z.enum(["auth", "payment"]),
    createdAt: z.number(),
  })
  .refine((value) => value.messageId || value.lockType, {
    message: "messageId or lockType is required",
  });

const PendingChatImageReturnSchema = z.object({
  reason: z.literal("image_paywall"),
  characterId: z.string().min(1),
  messageId: z.string().min(1),
  returnUrl: z
    .string()
    .min(1)
    .refine(
      (value) => value.startsWith("/") && !value.startsWith("//"),
      "returnUrl must be an internal route",
    ),
  createdAt: z.number(),
});

export type PendingChatUnlock = z.output<typeof PendingChatUnlockSchema>;
export type PendingChatPromotion = z.output<
  typeof PendingChatPromotionSchema
>;
export type PendingChatImageReturn = z.output<
  typeof PendingChatImageReturnSchema
>;

export function createPendingChatPromotion(
  promotionType: PendingChatPromotionType,
  characterId: string,
): PendingChatPromotion {
  return PendingChatPromotionSchema.parse({
    characterId,
    promotionType,
    lockType: toPromotionLockType(promotionType),
    clientLockId: `promotion_${createUuidV4()}`,
    createdAt: Date.now(),
  });
}

/**
 * NavigationStorage owns short-lived cross-route sessions.
 *
 * UI / router code should not touch the raw session storage utility directly.
 * Keeping these payloads here makes auth, payment, and chat return flows
 * testable without spreading storage keys across screens.
 */
export class NavigationStorage {
  private constructor() {}

  static async savePendingChatUnlock(input: {
    characterId: string;
    displayMessageId?: string;
    messageId?: string;
    kind: PendingChatUnlockKind;
    lockType?: PendingChatUnlock["lockType"];
    clientLockId?: string;
    promotion?: PendingChatPromotion;
    returnUrl: string;
    stage: PendingChatUnlockStage;
  }): Promise<void> {
    const displayMessageId =
      input.displayMessageId ??
      input.messageId ??
      (input.clientLockId ? `promotion:${input.clientLockId}` : null);
    if (!displayMessageId) {
      throw new Error("displayMessageId is required");
    }
    if (
      input.promotion &&
      input.promotion.characterId !== input.characterId
    ) {
      throw new Error("Pending promotion belongs to a different character.");
    }
    const payload: PendingChatUnlock = {
      reason: "single_message_unlock",
      characterId: input.characterId,
      displayMessageId,
      ...(input.messageId ? { messageId: input.messageId } : {}),
      kind: input.kind,
      ...(input.lockType ? { lockType: input.lockType } : {}),
      ...(input.clientLockId ? { clientLockId: input.clientLockId } : {}),
      ...(input.promotion ? { promotion: input.promotion } : {}),
      returnUrl: input.returnUrl,
      stage: input.stage,
      createdAt: Date.now(),
    };
    await SessionAsyncUtil.setJson(
      StorageKeys.pendingChatUnlock,
      payload,
      PendingChatUnlockSchema,
    );
  }

  static async consumePendingChatUnlock(
    characterId: string,
  ): Promise<PendingChatUnlock | null> {
    const result = await SessionAsyncUtil.getJson(
      StorageKeys.pendingChatUnlock,
      PendingChatUnlockSchema,
    );
    if (Result.isErr(result)) {
      await SessionAsyncUtil.remove(StorageKeys.pendingChatUnlock);
      return null;
    }
    const value = NavigationStorage.parsePendingChatUnlock(result.data);
    if (!value) {
      await SessionAsyncUtil.remove(StorageKeys.pendingChatUnlock);
      return null;
    }
    if (value.characterId !== normalizeLegacyCharacterId(characterId)) {
      return null;
    }
    await SessionAsyncUtil.remove(StorageKeys.pendingChatUnlock);
    return value;
  }

  static async peekPendingChatUnlock(
    characterId: string,
  ): Promise<PendingChatUnlock | null> {
    const result = await SessionAsyncUtil.getJson(
      StorageKeys.pendingChatUnlock,
      PendingChatUnlockSchema,
    );
    if (Result.isErr(result)) {
      await SessionAsyncUtil.remove(StorageKeys.pendingChatUnlock);
      return null;
    }
    const value = NavigationStorage.parsePendingChatUnlock(result.data);
    if (!value) {
      await SessionAsyncUtil.remove(StorageKeys.pendingChatUnlock);
    }
    return value?.characterId === normalizeLegacyCharacterId(characterId)
      ? value
      : null;
  }

  static async hasPendingChatUnlock(characterId: string): Promise<boolean> {
    return (
      (await NavigationStorage.peekPendingChatUnlock(characterId)) !== null
    );
  }

  static async clearPendingChatUnlock(): Promise<void> {
    await SessionAsyncUtil.remove(StorageKeys.pendingChatUnlock);
  }

  static async savePendingChatPromotion(
    promotionType: PendingChatPromotionType,
    characterId: string,
  ): Promise<PendingChatPromotion> {
    const promotion = createPendingChatPromotion(
      promotionType,
      characterId,
    );
    await SessionAsyncUtil.setJson(
      StorageKeys.pendingChatPromotion,
      promotion,
      PendingChatPromotionSchema,
    );
    return promotion;
  }

  static async consumePendingChatPromotion(
    characterId: string,
  ): Promise<PendingChatPromotion | null> {
    const result = await SessionAsyncUtil.getJson(
      StorageKeys.pendingChatPromotion,
      PendingChatPromotionSchema,
    );
    if (Result.isErr(result)) {
      await SessionAsyncUtil.remove(StorageKeys.pendingChatPromotion);
      return null;
    }
    const value = NavigationStorage.parsePendingChatPromotion(result.data);
    if (!value) {
      await SessionAsyncUtil.remove(StorageKeys.pendingChatPromotion);
      return null;
    }
    if (value.characterId !== normalizeLegacyCharacterId(characterId)) {
      return null;
    }
    await SessionAsyncUtil.remove(StorageKeys.pendingChatPromotion);
    return value;
  }

  static async clearPendingChatPromotion(): Promise<void> {
    await SessionAsyncUtil.remove(StorageKeys.pendingChatPromotion);
  }

  static async savePendingChatImageReturn(input: {
    characterId: string;
    messageId: string;
    returnUrl: string;
  }): Promise<void> {
    const payload: PendingChatImageReturn = {
      reason: "image_paywall",
      characterId: input.characterId,
      messageId: input.messageId,
      returnUrl: input.returnUrl,
      createdAt: Date.now(),
    };
    await SessionAsyncUtil.setJson(
      StorageKeys.pendingChatImageReturn,
      payload,
      PendingChatImageReturnSchema,
    );
  }

  static async consumePendingChatImageReturn(
    characterId: string,
  ): Promise<PendingChatImageReturn | null> {
    const result = await SessionAsyncUtil.getJson(
      StorageKeys.pendingChatImageReturn,
      PendingChatImageReturnSchema,
    );
    if (Result.isErr(result)) {
      await SessionAsyncUtil.remove(StorageKeys.pendingChatImageReturn);
      return null;
    }
    const value = NavigationStorage.parsePendingChatImageReturn(result.data);
    if (!value) {
      await SessionAsyncUtil.remove(StorageKeys.pendingChatImageReturn);
      return null;
    }
    if (value.characterId !== normalizeLegacyCharacterId(characterId)) {
      return null;
    }
    await SessionAsyncUtil.remove(StorageKeys.pendingChatImageReturn);
    return value;
  }

  static async clearPendingChatImageReturn(): Promise<void> {
    await SessionAsyncUtil.remove(StorageKeys.pendingChatImageReturn);
  }

  private static parsePendingChatUnlock(
    value: PendingChatUnlock | null,
  ): PendingChatUnlock | null {
    if (!value || Date.now() - value.createdAt > MAX_AGE_MS) return null;
    const characterId = normalizeLegacyCharacterId(value.characterId);
    if (!getCharacterById(characterId)) return null;
    const promotion = value.promotion
      ? NavigationStorage.parsePendingChatPromotion(value.promotion)
      : undefined;
    if (value.promotion && !promotion) return null;
    return PendingChatUnlockSchema.parse({
      ...value,
      characterId,
      ...(promotion ? { promotion } : {}),
    });
  }

  private static parsePendingChatImageReturn(
    value: PendingChatImageReturn | null,
  ): PendingChatImageReturn | null {
    if (!value || Date.now() - value.createdAt > MAX_AGE_MS) return null;
    const characterId = normalizeLegacyCharacterId(value.characterId);
    return getCharacterById(characterId)
      ? PendingChatImageReturnSchema.parse({ ...value, characterId })
      : null;
  }

  private static parsePendingChatPromotion(
    value: PendingChatPromotion | null,
  ): PendingChatPromotion | null {
    if (!value || Date.now() - value.createdAt > MAX_AGE_MS) return null;
    const characterId = normalizeLegacyCharacterId(value.characterId);
    return getCharacterById(characterId)
      ? PendingChatPromotionSchema.parse({ ...value, characterId })
      : null;
  }
}

function toPromotionLockType(
  promotionType: PendingChatPromotionType,
): PendingChatPromotion["lockType"] {
  if (promotionType === "voice") return "voice_message";
  if (promotionType === "image") return "image_paywall";
  return "private_message";
}

function createUuidV4(): string {
  const cryptoApi = globalThis.crypto;
  if (typeof cryptoApi?.randomUUID === "function") {
    return cryptoApi.randomUUID();
  }

  const bytes = new Uint8Array(16);
  if (typeof cryptoApi?.getRandomValues === "function") {
    cryptoApi.getRandomValues(bytes);
  } else {
    for (let index = 0; index < bytes.length; index += 1) {
      bytes[index] = Math.floor(Math.random() * 256);
    }
  }
  bytes[6] = (bytes[6] & 0x0f) | 0x40;
  bytes[8] = (bytes[8] & 0x3f) | 0x80;
  const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0"));
  return [
    hex.slice(0, 4).join(""),
    hex.slice(4, 6).join(""),
    hex.slice(6, 8).join(""),
    hex.slice(8, 10).join(""),
    hex.slice(10, 16).join(""),
  ].join("-");
}
