import { randomUUID } from "node:crypto";
import { QbitClient } from "../qbit/qbit.client";
import { readDb, writeDb } from "../storage/jsondb";
import { LoopJob } from "../types";
import { nowIso } from "../utils/time";
import { appendAuditLog, logger } from "../utils/logger";
import { emitJobLog, emitJobMetrics } from "../realtime/emitter";
import { appendLoopLog, archiveLoopLogs } from "../storage/loopLogs";
import { config } from "../config";

const logJob = async (jobId: string, level: "INFO" | "WARN" | "ERROR", message: string, event?: string) => {
  const createdAt = nowIso();
  emitJobLog({ jobId, level, message, createdAt });
  await appendLoopLog({ jobId, level, message, createdAt });
  if (event) {
    await appendAuditLog({ level, event: event as any, message });
  }
};

export const createLoopJob = async (
  input: {
    torrentHash: string;
    name: string;
    sizeBytes: number;
    magnet?: string;
    torrentFilePath?: string;
    allowIp: string;
    targetLoops: number;
    delayMs: number;
    profileName?: string;
    profileId?: string;
  }
): Promise<LoopJob> => {
  const now = nowIso();
  const job: LoopJob = {
    id: randomUUID(),
    torrentHash: input.torrentHash,
    name: input.name,
    sizeBytes: input.sizeBytes,
    magnet: input.magnet,
    torrentFilePath: input.torrentFilePath,
    allowIp: input.allowIp,
    targetLoops: input.targetLoops,
    doneLoops: 0,
    delayMs: input.delayMs,
    profileName: input.profileName,
    profileId: input.profileId,
    deleteDataBetweenLoops: true,
    enforcementMode: "aggressive-soft",
    status: "RUNNING",
    currentRun: {
      startedAt: now,
      lastProgress: 0,
      lastProgressAt: now,
      downloadedThisRunBytes: 0,
      avgSpeed: 0,
    },
    totals: {
      totalDownloadedBytes: 0,
      totalTimeMs: 0,
    },
    bans: {
      bannedIps: [],
    },
    createdAt: now,
    updatedAt: now,
  };
  const db = await readDb();
  const active = db.loopJobs.filter((j) => j.torrentHash === job.torrentHash && j.status !== "COMPLETED" && j.status !== "STOPPED");
  for (const existing of active) {
    existing.status = "STOPPED";
    existing.nextRunAt = undefined;
    existing.currentRun = undefined;
    existing.updatedAt = nowIso();
  }
  if (active.length) {
    await writeDb(db);
  }
  db.loopJobs.push(job);
  await writeDb(db);
  await logJob(job.id, "INFO", `Loop job started for ${job.name}`, "JOB_STARTED");
  return job;
};

export const stopLoopJob = async (jobId: string) => {
  const db = await readDb();
  const job = db.loopJobs.find((j) => j.id === jobId);
  if (!job) {
    return null;
  }
  job.status = "STOPPED";
  job.nextRunAt = undefined;
  job.currentRun = undefined;
  job.updatedAt = nowIso();
  await writeDb(db);
  await logJob(job.id, "WARN", "Loop job stopped by user");
  await archiveLoopLogs(job.id, job.torrentHash);
  return job;
};

export const updateJob = async (job: LoopJob) => {
  const db = await readDb();
  const index = db.loopJobs.findIndex((j) => j.id === job.id);
  if (index === -1) {
    return null;
  }
  db.loopJobs[index] = job;
  await writeDb(db);
  emitJobMetrics(job);
  return job;
};

export const tickLoopJobs = async (
  qbit: QbitClient,
  torrents: { hash: string; progress: number; state: string; dlspeed: number }[]
) => {
  const db = await readDb();
  let changed = false;

  for (const job of db.loopJobs) {
    if (job.status === "RUNNING") {
      const torrent = torrents.find((t) => t.hash === job.torrentHash);
      if (!torrent) {
        try {
          if (job.torrentFilePath) {
            await qbit.addTorrentByFile(job.torrentFilePath);
          } else if (job.magnet) {
            await qbit.addTorrentByMagnet(job.magnet);
          }
          await logJob(job.id, "WARN", "Torrent missing, re-added", "JOB_RESTARTED");
        } catch (error) {
          job.status = "ERROR";
          job.lastError = "Failed to re-add torrent";
          await logJob(job.id, "ERROR", job.lastError);
        }
        job.updatedAt = nowIso();
        changed = true;
        continue;
      }

      job.currentRun = job.currentRun ?? {
        startedAt: nowIso(),
        lastProgress: 0,
        lastProgressAt: nowIso(),
        downloadedThisRunBytes: 0,
        avgSpeed: 0,
      };
      if (torrent.progress > job.currentRun.lastProgress) {
        job.currentRun.lastProgress = torrent.progress;
        job.currentRun.lastProgressAt = nowIso();
        job.currentRun.stalledSince = undefined;
      }
      job.currentRun.avgSpeed = torrent.dlspeed;
      job.updatedAt = nowIso();

      const stalledState = /stalled|meta/i.test(torrent.state);
      if (stalledState) {
        if (!job.currentRun.stalledSince) {
          job.currentRun.stalledSince = nowIso();
        }
        const lastProgressAt = job.currentRun.lastProgressAt
          ? new Date(job.currentRun.lastProgressAt).getTime()
          : 0;
        if (Date.now() - lastProgressAt > config.stalledRecoveryMs) {
          await logJob(
            job.id,
            "WARN",
            "Stalled recovery: torrent will be removed and re-added"
          );
          try {
            await qbit.deleteTorrent(job.torrentHash, true);
          } catch (error) {
            logger.error({ error }, "Failed to delete stalled torrent");
          }
          job.status = "WAITING_DELAY";
          job.nextRunAt = new Date(Date.now() + job.delayMs).toISOString();
          job.updatedAt = nowIso();
          changed = true;
          continue;
        }
      }

      if (torrent.progress >= 1 && /UP|uploading|stalledUP|pausedUP|queuedUP/i.test(torrent.state)) {
        job.doneLoops += 1;
        job.totals.totalDownloadedBytes += job.sizeBytes;
        await logJob(job.id, "INFO", `Loop ${job.doneLoops} completed`, "JOB_COMPLETED_LOOP");
        try {
          await qbit.deleteTorrent(job.torrentHash, true);
        } catch (error) {
          logger.error({ error }, "Failed to delete torrent");
        }
        if (job.doneLoops >= job.targetLoops) {
          job.status = "COMPLETED";
          await logJob(job.id, "INFO", "All loops completed", "JOB_COMPLETED_ALL");
        } else {
          job.status = "WAITING_DELAY";
          job.nextRunAt = new Date(Date.now() + job.delayMs).toISOString();
        }
      }
      changed = true;
    }

    if (job.status === "WAITING_DELAY") {
      const nextRunAt = job.nextRunAt ? new Date(job.nextRunAt).getTime() : 0;
      if (Date.now() >= nextRunAt) {
        try {
          if (job.torrentFilePath) {
            await qbit.addTorrentByFile(job.torrentFilePath);
          } else if (job.magnet) {
            await qbit.addTorrentByMagnet(job.magnet);
          }
          job.status = "RUNNING";
          job.currentRun = {
            startedAt: nowIso(),
            lastProgress: 0,
            lastProgressAt: nowIso(),
            downloadedThisRunBytes: 0,
            avgSpeed: 0,
          };
          await logJob(job.id, "INFO", "Loop restarted", "JOB_RESTARTED");
        } catch (error) {
          job.status = "ERROR";
          job.lastError = "Failed to re-add torrent after delay";
          await logJob(job.id, "ERROR", job.lastError);
        }
        job.updatedAt = nowIso();
        changed = true;
      }
    }
  }

  if (changed) {
    await writeDb(db);
    db.loopJobs.forEach((job) => emitJobMetrics(job));
  }
};
