/**
 * image-gen/lib/task-store.js
 *
 * In-memory task metadata store with debounced atomic write to disk.
 * Memory is the authority; disk is a snapshot for restart recovery.
 *
 * Extracted from the dreamina plugin with two key changes:
 *   - submitId renamed to taskId throughout
 *   - adapterId added as a required field
 */

import fs from "node:fs";
import path from "node:path";

const DEBOUNCE_MS = 300;
const LEGACY_PROTOCOL_BY_ADAPTER = {
  openai: "openai-images",
  "openai-codex-oauth": "openai-codex-responses-image",
  volcengine: "volcengine-images",
  "volcengine-coding": "volcengine-images",
  dashscope: "dashscope-images",
  minimax: "minimax-images",
  gemini: "gemini-generate-content-image",
};

function normalizeLoadedTask(task) {
  if (!task || typeof task.taskId !== "string") return null;
  const providerId = task.providerId || task.adapterId || task.params?.providerId || null;
  const modelId = task.modelId || task.params?.modelId || task.params?.model || null;
  const protocolId = task.protocolId || task.params?.protocolId || LEGACY_PROTOCOL_BY_ADAPTER[providerId] || null;
  const deliveryMode = task.deliveryMode || task.delivery?.mode || "session";
  const params = {
    ...(task.params || {}),
    ...(providerId ? { providerId } : {}),
    ...(modelId ? { modelId, model: task.params?.model || modelId } : {}),
    ...(protocolId ? { protocolId } : {}),
    ...(task.credentialLaneId ? { credentialLaneId: task.credentialLaneId } : {}),
  };
  return {
    ...task,
    providerId,
    modelId,
    protocolId,
    credentialLaneId: task.credentialLaneId || task.params?.credentialLaneId || null,
    deliveryMode,
    delivery: task.delivery || { mode: deliveryMode },
    params,
  };
}

export class TaskStore {
  declare _dataDir: any;
  declare _debounceTimer: any;
  declare _filePath: any;
  declare _tasks: any;
  /**
   * @param {string} dataDir  Directory where tasks.json lives (created if absent)
   */
  constructor(dataDir) {
    this._dataDir = dataDir;
    this._filePath = path.join(dataDir, "tasks.json");
    /** @type {Map<string, object>} keyed by taskId */
    this._tasks = new Map();
    this._debounceTimer = null;
    this._load();
  }

  // ---------------------------------------------------------------------------
  // Write
  // ---------------------------------------------------------------------------

  /**
   * Add a new task. Throws if taskId already exists.
   *
   * @param {{ taskId: string, adapterId: string, providerId?: string|null, modelId?: string|null, protocolId?: string|null, credentialLaneId?: string|null, batchId: string, type: string, prompt: string, params: object, sessionPath?: string|null, deliveryMode?: string, delivery?: object|null, deliveryTarget?: object|null, metadata?: object|null, adapterTaskId?: string|null, submitState?: string }} opts
   */
  add({ taskId, adapterId, providerId = null, modelId = null, protocolId = null, credentialLaneId = null, batchId, type, prompt, params, sessionPath, deliveryMode = "session", delivery = null, deliveryTarget = null, metadata = null, adapterTaskId = null, submitState = "submitted" }) {
    if (this._tasks.has(taskId)) {
      throw new Error(`TaskStore: duplicate taskId "${taskId}"`);
    }
    const task = {
      taskId,
      adapterId,
      providerId: providerId || adapterId || null,
      modelId: modelId || params?.modelId || params?.model || null,
      protocolId: protocolId || params?.protocolId || null,
      credentialLaneId: credentialLaneId || params?.credentialLaneId || null,
      batchId,
      type,
      prompt,
      params,
      sessionPath: sessionPath || null,
      deliveryMode: deliveryMode || "session",
      delivery: delivery && typeof delivery === "object" && !Array.isArray(delivery)
        ? delivery
        : { mode: deliveryMode || "session" },
      deliveryTarget: deliveryTarget || null,
      metadata: metadata && typeof metadata === "object" && !Array.isArray(metadata) ? metadata : null,
      adapterTaskId: adapterTaskId || null,
      submitState,
      status: "pending",
      failReason: null,
      files: [],
      sessionFiles: [],
      favorited: false,
      imageWidth: null,
      imageHeight: null,
      createdAt: new Date().toISOString(),
      completedAt: null,
    };
    this._tasks.set(taskId, task);
    this._scheduleSave();
    return { ...task };
  }

  /**
   * Merge partial fields into an existing task.
   * Returns the updated shallow copy, or null if not found.
   *
   * @param {string} taskId
   * @param {object} patch
   */
  update(taskId, patch) {
    const task = this._tasks.get(taskId);
    if (!task) return null;
    Object.assign(task, patch);
    this._scheduleSave();
    return { ...task };
  }

  /**
   * Delete a task by taskId.
   * Returns true if removed, false if not found.
   *
   * @param {string} taskId
   */
  remove(taskId) {
    const existed = this._tasks.delete(taskId);
    if (existed) this._scheduleSave();
    return existed;
  }

  /**
   * Remove all non-pending, non-favorited tasks.
   * Returns an array of the removed task shallow copies so the caller
   * can clean up associated files on disk.
   *
   * @returns {object[]}
   */
  removeUnfavorited() {
    const removed = [];
    for (const [taskId, task] of this._tasks) {
      if (!task.favorited && task.status !== "pending") {
        removed.push({ ...task });
        this._tasks.delete(taskId);
      }
    }
    if (removed.length > 0) this._scheduleSave();
    return removed;
  }

  // ---------------------------------------------------------------------------
  // Read
  // ---------------------------------------------------------------------------

  /**
   * @param {string} taskId
   * @returns {object|null} shallow copy or null
   */
  get(taskId) {
    const task = this._tasks.get(taskId);
    return task ? { ...task } : null;
  }

  /**
   * All tasks belonging to a batchId, as shallow copies.
   *
   * @param {string} batchId
   * @returns {object[]}
   */
  getByBatch(batchId) {
    return this._filter((t) => t.batchId === batchId);
  }

  /**
   * All tasks belonging to a given adapterId, as shallow copies.
   *
   * @param {string} adapterId
   * @returns {object[]}
   */
  getByAdapter(adapterId) {
    return this._filter((t) => t.adapterId === adapterId);
  }

  /**
   * All tasks as shallow copies, insertion order.
   *
   * @returns {object[]}
   */
  listAll() {
    return this._filter(() => true);
  }

  /**
   * Tasks with status === "pending" as shallow copies.
   *
   * @returns {object[]}
   */
  listPending() {
    return this._filter((t) => t.status === "pending");
  }

  /**
   * Tasks with favorited === true as shallow copies.
   *
   * @returns {object[]}
   */
  listFavorited() {
    return this._filter((t) => t.favorited === true);
  }

  // ---------------------------------------------------------------------------
  // Persistence
  // ---------------------------------------------------------------------------

  /**
   * Flush pending write immediately (synchronous).
   * Useful before process exit or in tests.
   */
  flushSync() {
    if (this._debounceTimer !== null) {
      clearTimeout(this._debounceTimer);
      this._debounceTimer = null;
    }
    this._writeSync();
  }

  /**
   * Cancel any pending debounce timer. Call on plugin unload.
   */
  destroy() {
    if (this._debounceTimer !== null) {
      clearTimeout(this._debounceTimer);
      this._debounceTimer = null;
    }
  }

  // ---------------------------------------------------------------------------
  // Private
  // ---------------------------------------------------------------------------

  /** @param {(task: object) => boolean} predicate */
  _filter(predicate) {
    const result = [];
    for (const task of this._tasks.values()) {
      if (predicate(task)) result.push({ ...task });
    }
    return result;
  }

  _load() {
    try {
      if (fs.existsSync(this._filePath)) {
        const raw = fs.readFileSync(this._filePath, "utf8");
        const arr = JSON.parse(raw);
        if (Array.isArray(arr)) {
          let changed = false;
          for (const task of arr) {
            const normalized = normalizeLoadedTask(task);
            if (normalized) {
              if (normalized.providerId !== task.providerId || normalized.protocolId !== task.protocolId) {
                changed = true;
              }
              this._tasks.set(normalized.taskId, normalized);
            }
          }
          if (changed) this._writeSync();
        }
      }
    } catch {
      // Corrupted or missing file: start with empty store.
    }
  }

  _scheduleSave() {
    if (this._debounceTimer !== null) clearTimeout(this._debounceTimer);
    this._debounceTimer = setTimeout(() => {
      this._debounceTimer = null;
      this._writeSync();
    }, DEBOUNCE_MS);
  }

  _writeSync() {
    try {
      fs.mkdirSync(this._dataDir, { recursive: true });
      const tmp = this._filePath + ".tmp";
      fs.writeFileSync(tmp, JSON.stringify([...this._tasks.values()]), "utf8");
      fs.renameSync(tmp, this._filePath);
    } catch (err) {
      // Non-fatal: log to stderr but do not throw — memory remains authoritative.
      process.stderr.write(`TaskStore: write failed: ${err.message}\n`);
    }
  }
}
