import type { UnlockedPrivateMessageLocalPatch } from "@/data/repositories/interfaces";
import { ChatMessage, ChatMessageSchema } from "@/data/schemas/chat";
import { LocalChatStorage, LocalMessage } from "@/data/storage/chat";
import { Result } from "@/utils/result";

import {
  resolveChatCacheOwnerKey,
  type ChatCacheIdentityResolver,
} from "./chat_cache_identity";

type LocalMessageStorage = Pick<
  LocalChatStorage,
  | "saveMessage"
  | "replaceMessagesBySession"
  | "getAllMessagesBySession"
  | "clearMessagesBySession"
  | "getMessageCountBySession"
>;

export class ChatLocalMessageStore {
  constructor(
    private readonly localStorage: LocalMessageStorage,
    private readonly resolveIdentity: ChatCacheIdentityResolver = resolveChatCacheOwnerKey,
  ) {}

  async markMessageUnlocked(
    messageId: string,
    patch: UnlockedPrivateMessageLocalPatch = {},
    identity?: string,
  ): Promise<Result<void>> {
    return Result.wrap(async () => {
      const ownerKey = await this._requireIdentity(identity);
      const localResult = await this._getMessages(ownerKey);
      if (Result.isErr(localResult)) {
        throw localResult.error;
      }

      let changed = false;
      const updatedMessages = localResult.data.map((message) => {
        if (!shouldMarkMessageUnlocked(message, messageId)) return message;
        changed = true;
        return ChatMessageSchema.parse({
          ...message,
          content: normalizeUnlockedContent(patch.content, message.content),
          audioUrl: normalizeUnlockedAudioUrl(patch.audioUrl, message.audioUrl),
          image: patch.image ?? message.image,
          lockDetail: patch.lockDetail ?? {
            locked: false,
            reason: null,
          },
        });
      });

      if (!changed) return;

      const saveResult = await this._saveMessages(updatedMessages, ownerKey);
      if (Result.isErr(saveResult)) {
        throw saveResult.error;
      }
    });
  }

  async saveMessage(
    message: ChatMessage,
    identity?: string,
  ): Promise<Result<void>> {
    return Result.wrap(async () => {
      const ownerKey = await this._requireIdentity(identity);
      const result = await this.localStorage.saveMessage(
        this._chatToLocal(message, ownerKey),
      );
      if (Result.isErr(result)) throw result.error;
    });
  }

  async saveMessages(
    messages: readonly ChatMessage[],
    identity?: string,
  ): Promise<Result<void>> {
    return Result.wrap(async () => {
      const ownerKey = await this._requireIdentity(identity);
      const result = await this._saveMessages(messages, ownerKey);
      if (Result.isErr(result)) throw result.error;
    });
  }

  async getMessages(
    identity?: string,
  ): Promise<Result<readonly ChatMessage[]>> {
    return Result.wrap(async () => {
      const ownerKey = await this._requireIdentity(identity);
      const result = await this._getMessages(ownerKey);
      if (Result.isErr(result)) throw result.error;
      return result.data;
    });
  }

  async clearMessages(identity?: string): Promise<Result<void>> {
    return Result.wrap(async () => {
      const ownerKey = await this._requireIdentity(identity);
      const result = await this.localStorage.clearMessagesBySession(ownerKey);
      if (Result.isErr(result)) throw result.error;
    });
  }

  async getMessageCount(identity?: string): Promise<Result<number>> {
    return Result.wrap(async () => {
      const ownerKey = await this._requireIdentity(identity);
      const result = await this.localStorage.getMessageCountBySession(ownerKey);
      if (Result.isErr(result)) throw result.error;
      return result.data;
    });
  }

  private async _requireIdentity(identity?: string): Promise<string> {
    if (identity !== undefined) {
      if (identity.length === 0) {
        throw new Error("Chat cache identity must not be empty.");
      }
      return identity;
    }
    const result = await this.resolveIdentity();
    if (Result.isErr(result)) throw result.error;
    if (result.data.length === 0) {
      throw new Error("Chat cache identity must not be empty.");
    }
    return result.data;
  }

  private async _getMessages(
    identity: string,
  ): Promise<Result<readonly ChatMessage[]>> {
    const result = await this.localStorage.getAllMessagesBySession(identity);
    if (Result.isErr(result)) return result;
    return Result.ok(result.data.map((message) => this._localToChat(message)));
  }

  private _saveMessages(
    messages: readonly ChatMessage[],
    identity: string,
  ): Promise<Result<void>> {
    return this.localStorage.replaceMessagesBySession(
      identity,
      messages.map((message) => this._chatToLocal(message, identity)),
    );
  }

  private _chatToLocal(message: ChatMessage, identity: string): LocalMessage {
    return LocalMessage.from({
      id: message.id,
      role: message.role,
      type: message.type,
      content: message.content,
      createdAt: message.createdAt,
      audioUrl: message.audioUrl,
      image: message.image,
      lockDetail: message.lockDetail,
      sessionId: identity,
    });
  }

  private _localToChat(local: LocalMessage): ChatMessage {
    return ChatMessageSchema.parse({
      id: local.id,
      role: local.role,
      type: local.type,
      content: local.content,
      createdAt: local.createdAt,
      audioUrl: local.audioUrl,
      image: local.image,
      lockDetail: local.lockDetail,
    });
  }
}

function normalizeUnlockedContent(
  nextContent: string | undefined,
  currentContent: string,
): string {
  if (nextContent == null || nextContent.length === 0) return currentContent;
  return nextContent;
}

function normalizeUnlockedAudioUrl(
  nextAudioUrl: string | null | undefined,
  currentAudioUrl: string | null,
): string | null {
  if (nextAudioUrl == null || nextAudioUrl.length === 0) return currentAudioUrl;
  return nextAudioUrl;
}

function shouldMarkMessageUnlocked(
  message: ChatMessage,
  messageId: string,
): boolean {
  if (message.id !== messageId) return false;
  if (message.role !== "assistant") return false;
  if (message.lockDetail.locked !== true) return false;
  return (
    Boolean(message.image.url) ||
    message.lockDetail.reason === "image_paywall" ||
    message.lockDetail.reason === "image" ||
    message.lockDetail.reason === "private_message" ||
    message.lockDetail.reason === "voice_message"
  );
}
