import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
import path from "node:path";

export class Storage {
  constructor(baseDir) {
    this.baseDir = baseDir;
    this.runsDir = path.join(baseDir, "runs");
    this.rawDir = path.join(baseDir, "raw");
    this.skillsDir = path.join(baseDir, "skills");
    this.avoidDir = path.join(baseDir, "avoid");
    this.campaignsDir = path.join(baseDir, "campaigns");
    this.suppressionDir = path.join(baseDir, "suppression");
    this.runSaveQueues = new Map();
    this.jsonSaveQueues = new Map();
  }

  async init() {
    await Promise.all([
      mkdir(this.runsDir, { recursive: true }),
      mkdir(this.rawDir, { recursive: true }),
      mkdir(this.skillsDir, { recursive: true }),
      mkdir(this.avoidDir, { recursive: true }),
      mkdir(this.campaignsDir, { recursive: true }),
      mkdir(this.suppressionDir, { recursive: true })
    ]);
    await this.ensureJson(path.join(this.skillsDir, "hermes-skills.json"), []);
    await this.ensureJson(path.join(this.avoidDir, "hermes-avoid.json"), []);
    await this.ensureJson(path.join(this.suppressionDir, "emails.json"), []);
  }

  async ensureJson(filePath, initialValue) {
    try {
      JSON.parse(await readFile(filePath, "utf8"));
    } catch {
      await this.backupCorruptJson(filePath);
      await this.writeJsonAtomic(filePath, initialValue);
    }
  }

  runPath(id) {
    return path.join(this.runsDir, `${id}.json`);
  }

  async saveRun(run) {
    const id = run.id;
    const prior = this.runSaveQueues.get(id) || Promise.resolve();
    const next = prior.catch(() => {}).then(() => this.writeRunFile(run));
    this.runSaveQueues.set(id, next);
    try {
      await next;
    } finally {
      if (this.runSaveQueues.get(id) === next) this.runSaveQueues.delete(id);
    }
  }

  async writeRunFile(run) {
    const finalPath = this.runPath(run.id);
    const tempPath = `${finalPath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
    await writeFile(tempPath, JSON.stringify(run, null, 2));
    await rename(tempPath, finalPath);
  }

  async getRun(id) {
    try {
      return JSON.parse(await readFile(this.runPath(id), "utf8"));
    } catch {
      return null;
    }
  }

  async listRuns() {
    const files = await readdir(this.runsDir);
    const runs = await Promise.all(files.filter((file) => file.endsWith(".json")).map(async (file) => {
      const run = JSON.parse(await readFile(path.join(this.runsDir, file), "utf8"));
      return {
        id: run.id,
        status: run.status,
        niche: run.input?.niche,
        target: run.input?.target,
        qualifiedCount: run.qualifiedLeads?.length || 0,
        ownerHunts: run.scaleAudit?.ownerHunts || run.counters?.ownerRabbitHoles || 0,
        ownerAgentsDeployed: run.scaleAudit?.ownerAgentsDeployed || run.counters?.ownerAgentsDeployed || 0,
        ownerContacts: run.scaleAudit?.ownerContacts || run.counters?.ownerContactsFound || 0,
        ownerContactYieldPercent: run.scaleAudit?.ownerContactYieldPercent || 0,
        llmCalls: run.scaleAudit?.llmCalls || run.counters?.llmCalls || 0,
        llmTimeouts: run.scaleAudit?.llmTimeouts || run.counters?.llmTimeouts || 0,
        pendingDiscovered: run.scaleAudit?.pendingDiscovered || 0,
        createdAt: run.createdAt,
        updatedAt: run.updatedAt
      };
    }));
    return runs.sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)));
  }

  campaignPath(id) {
    return path.join(this.campaignsDir, `${id}.json`);
  }

  async saveCampaign(campaign) {
    await this.writeJsonAtomic(this.campaignPath(campaign.id), campaign);
  }

  async getCampaign(id) {
    return this.readJson(this.campaignPath(id), null);
  }

  async listCampaigns() {
    const files = await readdir(this.campaignsDir);
    const campaigns = await Promise.all(files.filter((file) => file.endsWith(".json")).map(async (file) => {
      const campaign = JSON.parse(await readFile(path.join(this.campaignsDir, file), "utf8"));
      return {
        id: campaign.id,
        runId: campaign.runId,
        status: campaign.status,
        backend: campaign.backend,
        name: campaign.name,
        subject: campaign.subject,
        recipients: campaign.recipients?.length || 0,
        tracking: campaign.tracking || {},
        createdAt: campaign.createdAt,
        updatedAt: campaign.updatedAt
      };
    }));
    return campaigns.sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)));
  }

  async getSuppressionList() {
    const entries = await this.readJson(path.join(this.suppressionDir, "emails.json"), []);
    return new Set((Array.isArray(entries) ? entries : []).map((entry) => String(entry.email || "").toLowerCase()).filter(Boolean));
  }

  async addSuppression(email, meta = {}) {
    const normalized = String(email || "").trim().toLowerCase();
    if (!normalized) return null;
    const filePath = path.join(this.suppressionDir, "emails.json");
    const prior = this.jsonSaveQueues.get(filePath) || Promise.resolve();
    const next = prior.catch(() => {}).then(async () => {
      const current = await this.readJsonForUpdate(filePath, []);
      const list = Array.isArray(current) ? current : [];
      if (!list.some((entry) => String(entry.email || "").toLowerCase() === normalized)) {
        list.unshift({ email: normalized, ...meta, createdAt: new Date().toISOString() });
      }
      await this.writeJsonAtomic(filePath, list.slice(0, 10000));
      return { email: normalized };
    });
    this.jsonSaveQueues.set(filePath, next);
    try {
      return await next;
    } finally {
      if (this.jsonSaveQueues.get(filePath) === next) this.jsonSaveQueues.delete(filePath);
    }
  }

  async listRecoverableRuns() {
    const files = await readdir(this.runsDir);
    const runs = [];
    for (const file of files.filter((item) => item.endsWith(".json"))) {
      try {
        const run = JSON.parse(await readFile(path.join(this.runsDir, file), "utf8"));
        if (["queued", "running", "interrupted"].includes(run.status)) runs.push(run);
      } catch {
        // Ignore corrupt run files; normal API reads will still return null.
      }
    }
    return runs.sort((a, b) => String(a.updatedAt || a.createdAt).localeCompare(String(b.updatedAt || b.createdAt)));
  }

  async getOperationalMemory({ niche = "", limit = 20 } = {}) {
    const [skills, avoid] = await Promise.all([
      this.readJson(path.join(this.skillsDir, "hermes-skills.json"), []),
      this.readJson(path.join(this.avoidDir, "hermes-avoid.json"), [])
    ]);
    const needle = String(niche || "").toLowerCase();
    const relevant = (item) => {
      if (!needle) return true;
      const text = JSON.stringify(item || {}).toLowerCase();
      return text.includes(needle) || !/(dentist|roofer|roofing|salon|spa|contractor|restaurant|lawyer|doctor)/i.test(text);
    };

    return {
      skills: skills.filter(relevant).slice(0, limit),
      avoid: avoid.filter(relevant).slice(0, limit)
    };
  }

  async readJson(filePath, fallback) {
    try {
      return JSON.parse(await readFile(filePath, "utf8"));
    } catch {
      return fallback;
    }
  }

  async appendSkill(skill) {
    const filePath = path.join(this.skillsDir, "hermes-skills.json");
    await this.appendJsonList(filePath, skill);
  }

  async appendAvoid(entry) {
    const filePath = path.join(this.avoidDir, "hermes-avoid.json");
    await this.appendJsonList(filePath, entry);
  }

  async appendJsonList(filePath, entry) {
    const prior = this.jsonSaveQueues.get(filePath) || Promise.resolve();
    const next = prior.catch(() => {}).then(async () => {
      const current = await this.readJsonForUpdate(filePath, []);
      const list = Array.isArray(current) ? current : [];
      list.unshift({ ...entry, createdAt: new Date().toISOString() });
      await this.writeJsonAtomic(filePath, list.slice(0, 500));
    });
    this.jsonSaveQueues.set(filePath, next);
    try {
      await next;
    } finally {
      if (this.jsonSaveQueues.get(filePath) === next) this.jsonSaveQueues.delete(filePath);
    }
  }

  async readJsonForUpdate(filePath, fallback) {
    try {
      return JSON.parse(await readFile(filePath, "utf8"));
    } catch {
      await this.backupCorruptJson(filePath);
      return fallback;
    }
  }

  async writeJsonAtomic(filePath, value) {
    const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
    await writeFile(tempPath, JSON.stringify(value, null, 2));
    await rename(tempPath, filePath);
  }

  async backupCorruptJson(filePath) {
    try {
      await rename(filePath, `${filePath}.corrupt.${Date.now()}`);
    } catch {
      // Missing files are initialized by the caller; unreadable files fall back safely.
    }
  }
}
