import type { ChatMessage, ChatSendResponse } from "@/data/dto/chat";
import {
  LocalChatMediaStorage,
  type LocalChatMediaRow,
} from "@/data/storage/chat";
import { requestBrowserPersistentStorageOnce } from "@/lib/browser/persistent_storage";
import { Logger } from "@/utils/logger";
import { Result } from "@/utils/result";
import type {
  CacheRemoteChatMediaInput,
  ChatMediaLookupInput,
} from "@/data/repositories/interfaces";

import {
  buildChatMediaCacheKey,
  fallbackChatMediaMimeType,
  getMessageMediaTargets,
  getSendResponseMediaTargets,
  isCacheableRemoteChatMediaUrl,
  uniqueMediaTargets,
} from "./chat_media_cache_helpers";
import {
  resolveChatCacheOwnerKey,
  type ChatCacheIdentityResolver,
} from "./chat_cache_identity";

const log = new Logger("DataRepositoriesChatMediaCacheCoordinator");

class ChatMediaDownloadError extends Error {
  override readonly cause?: unknown;
  readonly status?: number;

  constructor(message: string, options?: { cause?: unknown; status?: number }) {
    super(message);
    this.name = "ChatMediaDownloadError";
    this.cause = options?.cause;
    this.status = options?.status;
  }
}

function isChatMediaDownloadError(error: Error): boolean {
  if (error instanceof ChatMediaDownloadError) return true;
  return error.cause instanceof ChatMediaDownloadError;
}

type ChatMediaStorage = Pick<
  LocalChatMediaStorage,
  "getMedia" | "saveMedia"
>;

export class ChatMediaCacheCoordinator {
  constructor(
    private readonly mediaStorage: ChatMediaStorage,
    private readonly resolveIdentity: ChatCacheIdentityResolver =
      resolveChatCacheOwnerKey,
  ) {}

  async getCachedMedia(
    input: ChatMediaLookupInput,
  ): Promise<Result<LocalChatMediaRow | null>> {
    return Result.wrap(async () => {
      const ownerKey = await this._requireIdentity();
      const cacheKey = this._buildMediaCacheKey(ownerKey, input);
      const result = await this.mediaStorage.getMedia(cacheKey);
      if (Result.isErr(result)) throw result.error;
      return result.data;
    });
  }

  async cacheRemoteMedia(
    input: CacheRemoteChatMediaInput,
    identity?: string,
  ): Promise<Result<LocalChatMediaRow>> {
    return Result.wrap(
      async () => {
        const ownerKey = await this._requireIdentity(identity);
        return this._cacheRemoteMediaOrThrow(input, ownerKey);
      },
      {
        errorLogLevel: (error) =>
          isChatMediaDownloadError(error) ? "info" : "error",
      },
    );
  }

  async prefetchMediaForMessages(
    messages: readonly ChatMessage[],
    identity?: string,
  ): Promise<Result<void>> {
    return this._prefetchMediaTargets(
      messages.flatMap((message) => getMessageMediaTargets(message)),
      identity,
    );
  }

  async prefetchMediaForSendResponse(
    response: ChatSendResponse,
    identity?: string,
  ): Promise<Result<void>> {
    return this._prefetchMediaTargets(
      getSendResponseMediaTargets(response),
      identity,
    );
  }

  private async _prefetchMediaTargets(
    targets: readonly CacheRemoteChatMediaInput[],
    identity?: string,
  ): Promise<Result<void>> {
    return Result.wrap(async () => {
      const uniqueTargets = uniqueMediaTargets(targets);
      if (uniqueTargets.length === 0) return;
      const ownerKey = await this._requireIdentity(identity);
      for (const target of uniqueTargets) {
        try {
          await this._cacheRemoteMediaOrThrow(target, ownerKey);
        } catch (error) {
          const details = {
            messageId: target.messageId,
            kind: target.kind,
            error,
          };
          if (error instanceof ChatMediaDownloadError) {
            log.info("[chat-media] remote media unavailable during prefetch", {
              ...details,
              status: error.status,
            });
          } else {
            log.warn("[chat-media] prefetch failed", details);
          }
        }
      }
    });
  }

  private async _cacheRemoteMediaOrThrow(
    input: CacheRemoteChatMediaInput,
    ownerKey: string,
  ): Promise<LocalChatMediaRow> {
    if (!isCacheableRemoteChatMediaUrl(input.remoteUrl)) {
      throw new Error(`Unsupported media url: ${input.remoteUrl}`);
    }

    const cacheKey = this._buildMediaCacheKey(ownerKey, input);
    const existingResult = await this.mediaStorage.getMedia(cacheKey);
    if (Result.isErr(existingResult)) throw existingResult.error;
    if (existingResult.data) return existingResult.data;

    let response: Response;
    try {
      response = await fetch(input.remoteUrl, {
        method: "GET",
        cache: "force-cache",
      });
    } catch (error) {
      throw new ChatMediaDownloadError("Media download request failed.", {
        cause: error,
      });
    }
    if (!response.ok) {
      throw new ChatMediaDownloadError(
        `Media download failed: ${response.status} ${response.statusText}`,
        { status: response.status },
      );
    }

    let bytes: ArrayBuffer;
    try {
      bytes = await response.arrayBuffer();
    } catch (error) {
      throw new ChatMediaDownloadError("Media download body could not be read.", {
        cause: error,
      });
    }
    if (bytes.byteLength <= 0) {
      throw new ChatMediaDownloadError("Media download returned empty bytes.");
    }
    const mimeType =
      response.headers.get("content-type") ||
      fallbackChatMediaMimeType(input.kind);

    const saveResult = await this.mediaStorage.saveMedia({
      cacheKey,
      ownerKey,
      messageId: input.messageId,
      kind: input.kind,
      remoteUrl: input.remoteUrl,
      bytes,
      mimeType,
    });
    if (Result.isErr(saveResult)) throw saveResult.error;
    void requestBrowserPersistentStorageOnce();

    const savedResult = await this.mediaStorage.getMedia(cacheKey);
    if (Result.isErr(savedResult)) throw savedResult.error;
    if (!savedResult.data) {
      throw new Error("Cached media could not be read after save.");
    }
    return savedResult.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 _buildMediaCacheKey(
    ownerKey: string,
    input: ChatMediaLookupInput,
  ): string {
    return buildChatMediaCacheKey({
      ownerKey,
      messageId: input.messageId,
      kind: input.kind,
    });
  }
}
