// ============================================================
// infrastructure/storage/media-loader.ts — 媒体加载与缓存
//
// 职责：
//   - 通过认证 axios 下载媒体文件（携带 Authorization 头）
//   - 从 HTTP Content-Type 检测真实 MIME 类型
//      → 解决 CDN 直连链接无文件扩展名导致 isVideoUrl() 失效的问题
//   - 本地磁盘缓存 + 数据库元数据记录
//   - 返回 blob URL 供 <img>/<video> 标签使用
//
// 流程：
//   加载：检查本地缓存 → 命中 → 读文件 → blob URL
//                        → 未命中 → axios 下载 → 保存磁盘 → 记录 DB → blob URL
//   显示：blob URL（高效，支持视频 seek，无需 data: URI 编码开销）
// ============================================================

import axios from 'axios';
import { axiosInstance } from '@/shared/services/http';
import { logger } from '@/shared/utils/bus';
import { hasIpc } from '@/shared/utils/bus';
import { getCurrentUserId, hashURL, getTodayKey } from './user-scope';
import { getMediaByUrl, upsertMediaCache, deleteMediaByUrl } from '../stores/media-store';
import { saveBlobToFile, readFile, fileExists } from '../ipc-file';

// ---------- 类型 ----------

/** 加载成功 */
export interface MediaLoadSuccess {
  ok: true;
  /** 可直传 <img src> / <video src> 的 blob URL */
  blobUrl: string;
  /** HTTP 响应 Content-Type（如 "image/png"、"video/mp4"） */
  mimeType: string;
  /** 是否来自本地缓存（false = 本次网络下载） */
  fromCache: boolean;
}

/** 加载失败（含可区分的原因，供 UI 展示不同提示） */
export interface MediaLoadFailure {
  ok: false;
  reason: 'signature_expired' | 'network_error' | 'cancelled';
  message: string;
}

/** 判别联合：调用方通过 result.ok 收窄类型 */
export type MediaLoadResult = MediaLoadSuccess | MediaLoadFailure;

// ---------- 类型守卫 ----------

export function isMediaSuccess(r: MediaLoadResult): r is MediaLoadSuccess {
  return r.ok;
}

export function isMediaFailure(r: MediaLoadResult): r is MediaLoadFailure {
  return !r.ok;
}

// ---------- 内部工具 ----------

/**
 * base64 → Blob → blob URL
 *
 * 相比 data: URI（readFileAsBlobUrl 的做法），blob URL 优势：
 *   - 无 base64 33% 体积膨胀（内存友好）
 *   - <video> 可正常 seek/stream
 *   - 通过 URL.revokeObjectURL() 显式释放
 */
function base64ToBlobUrl(base64: string, mimeType: string): string {
  const binary = atob(base64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  const blob = new Blob([bytes], { type: mimeType });
  return URL.createObjectURL(blob);
}

/**
 * 从本地磁盘缓存加载媒体，返回 blob URL。
 * 若文件不存在或 IPC 不可用则返回 null。
 */
async function loadFromDisk(localPath: string, mimeType: string): Promise<string | null> {
  if (!hasIpc()) return null;
  try {
    const exists = await fileExists(localPath);
    if (!exists) return null;

    const base64 = await readFile(localPath);
    if (!base64) return null;

    return base64ToBlobUrl(base64, mimeType);
  } catch {
    return null;
  }
}

/**
 * 从远程下载媒体 → 保存到本地磁盘缓存 → 返回 blob URL。
 *
 * 降级策略：
 *   - 磁盘写入失败 → 仍返回 blob URL（本次会话可用，不持久化）
 *   - CDN 签名过期 (403 SignatureExpired) → 返回 MediaLoadFailure（reason: 'signature_expired'）
 *   - 其他网络错误 → 返回 MediaLoadFailure（reason: 'network_error'）
 *   - 请求取消 → 返回 MediaLoadFailure（reason: 'cancelled'）
 */
async function downloadAndCache(
  url: string,
  signal?: AbortSignal,
): Promise<MediaLoadSuccess | MediaLoadFailure> {
  try {
    // ---- 下载（axiosInstance 自动携带 Authorization） ----
    const response = await axiosInstance.get(url, {
      responseType: 'arraybuffer',
      signal,
      timeout: 120_000, // 媒体文件可能较大
    });

    // ---- 从 Content-Type 提取 MIME ----
    const rawContentType = String(response.headers['content-type'] || '');
    const mimeType = rawContentType.split(';')[0]?.trim() || 'application/octet-stream';

    const buffer = response.data as ArrayBuffer;
    const blob = new Blob([buffer], { type: mimeType });
    const blobUrl = URL.createObjectURL(blob);

    // ---- 写入磁盘缓存 ----
    if (hasIpc() && window.fileStorage) {
      const uid = getCurrentUserId();
      const ext = mimeType.split('/')[1] || 'bin';
      const urlHash = hashURL(url);
      const localPath = `media-cache/${uid}/${getTodayKey()}/${urlHash}.${ext}`;

      try {
        const savedPath = await saveBlobToFile(localPath, blob);
        if (savedPath) {
          upsertMediaCache(url, mimeType, buffer.byteLength, localPath);
          logger.debug('storage', `媒体已缓存：${mimeType} ${buffer.byteLength} bytes → ${localPath}`);
        }
      } catch (err) {
        logger.warn('storage', '媒体磁盘缓存写入失败（降级为内存模式）', err instanceof Error ? err : undefined);
      }
    }

    return { ok: true, blobUrl, mimeType, fromCache: false };
  } catch (err) {
    // 请求取消
    if (axios.isCancel(err) || (err as Record<string, unknown>)?.code === 'ERR_CANCELED') {
      return { ok: false, reason: 'cancelled', message: '请求已取消' };
    }

    // CDN 签名过期 — 403 + JSON body { code: "SignatureExpired", message: "..." }
    if (axios.isAxiosError(err) && err.response?.status === 403) {
      try {
        const raw = err.response.data;
        // responseType 为 arraybuffer，错误响应体也是 ArrayBuffer，需解码
        let text: string;
        if (raw instanceof ArrayBuffer) {
          text = new TextDecoder().decode(raw);
        } else if (typeof raw === 'string') {
          text = raw;
        } else {
          text = JSON.stringify(raw);
        }

        const body = JSON.parse(text) as Record<string, unknown>;
        if (body.code === 'SignatureExpired') {
          logger.debug('storage', `CDN 签名已过期：${url}`);
          return {
            ok: false,
            reason: 'signature_expired',
            message: '媒体链接签名已过期（有效期24小时），请重新生成任务',
          };
        }
      } catch {
        // 解析失败 → 按普通 403 处理
      }

      logger.warn('storage', `CDN 访问拒绝 (403)：${url}`);
      return {
        ok: false,
        reason: 'network_error',
        message: 'CDN 访问被拒绝（403），链接可能已失效',
      };
    }

    logger.warn('storage', `媒体下载失败：${url}`, err instanceof Error ? err : undefined);
    return {
      ok: false,
      reason: 'network_error',
      message: '媒体文件下载失败，请检查网络连接',
    };
  }
}

// ---------- 公开 API ----------

/**
 * 加载媒体文件，返回 blob URL 或可区分的错误信息。
 *
 * 策略：
 *   1. 查本地 DB 缓存 → 命中则读磁盘返回 blob URL
 *   2. 未命中则通过认证 HTTP 下载 → 写磁盘 + 记 DB → 返回 blob URL
 *   3. CDN 签名过期(403 SignatureExpired) → 返回 reason='signature_expired'
 *   4. 其他网络错误 → 返回 reason='network_error'
 *
 * @param url    - 媒体文件的完整 URL（可以是 CDN 直连链接）
 * @param signal - 取消信号（组件卸载时中止下载）
 */
export async function loadMedia(url: string, signal?: AbortSignal): Promise<MediaLoadResult> {
  // ---- 1. 检查本地数据库缓存 ----
  const cached = getMediaByUrl(url);
  if (cached) {
    const blobUrl = await loadFromDisk(cached.local_path, cached.mime_type);
    if (blobUrl) {
      logger.debug('storage', `媒体缓存命中：${url}`);
      return { ok: true, blobUrl, mimeType: cached.mime_type, fromCache: true };
    }
    // 数据库有记录但磁盘文件丢失 → 清理脏记录，走下载
    deleteMediaByUrl(url);
    logger.debug('storage', `媒体缓存文件丢失，重新下载：${url}`);
  }

  // ---- 2. 网络下载 + 缓存 ----
  return downloadAndCache(url, signal);
}

/**
 * 预加载一批媒体 URL（并行下载，不阻塞 UI）。
 * 适用于任务列表打开时预加载缩略图等场景。
 */
export function preloadMedia(urls: string[], signal?: AbortSignal): void {
  Promise.allSettled(urls.map(url => loadMedia(url, signal))).then(results => {
    const loaded = results.filter(r => r.status === 'fulfilled' && r.value.ok).length;
    if (loaded > 0) {
      logger.debug('storage', `预加载完成：${loaded}/${urls.length} 个媒体文件`);
    }
  });
}

// ---------- MIME 类型判断 ----------

/** 判断 MIME 是否为视频 */
export function isVideoMime(mimeType: string): boolean {
  return mimeType.startsWith('video/');
}

/** 判断 MIME 是否为图片 */
export function isImageMime(mimeType: string): boolean {
  return mimeType.startsWith('image/');
}

/** 判断 MIME 是否为音频 */
export function isAudioMime(mimeType: string): boolean {
  return mimeType.startsWith('audio/');
}

// ---------- 资源释放 ----------

/**
 * 释放 blob URL 占用的内存。
 * 应在组件卸载或 URL 不再使用时调用。
 */
export function revokeMediaUrl(blobUrl: string): void {
  if (blobUrl.startsWith('blob:')) {
    URL.revokeObjectURL(blobUrl);
  }
}
