import {
  Injectable,
  InternalServerErrorException,
  Logger,
  NotFoundException,
  OnModuleInit,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { randomUUID } from "node:crypto";
import { execFile } from "node:child_process";
import { access, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { basename, join } from "node:path";
import type { Response } from "express";
import chokidar from "chokidar";

export interface QueueNotification {
  id: string;
  agent: string;
  filename: string;
  payload: unknown;
  createdAt: Date;
}

export interface QueueTask {
  id: string;
  project: string;
  taskId: string;
  status: string;
  description: string;
}

interface AgentStateRecord {
  taskId: string;
  agent: string;
  branch: string;
}

interface AgentCiNotification {
  id: string;
  taskId: string;
  event: "completed" | "failed";
  targetAgent: string;
  fromAgent: string;
  payload: {
    branch: string;
    buildUrl: string;
    repo: string;
    ciStatus: "success" | "failure";
  };
  createdAt: string;
  retries: number;
  maxRetries: number;
  ttlSeconds: number;
}

@Injectable()
export class QueueNotificationsService implements OnModuleInit {
  private readonly logger = new Logger(QueueNotificationsService.name);

  constructor(private readonly configService: ConfigService) {}

  async onModuleInit(): Promise<void> {
    if (!(await this.inboxDirExists())) {
      this.logger.warn(`Queue notifications inbox directory does not exist: ${this.getInboxDir()}`);
    }
  }

  async listNotifications(): Promise<QueueNotification[]> {
    const inboxDir = this.getInboxDir();

    if (!(await this.inboxDirExists())) {
      return [];
    }

    // Paths come from controlled config plus directory entries under the inbox root.
    // eslint-disable-next-line security/detect-non-literal-fs-filename
    const agentEntries = await readdir(inboxDir, { withFileTypes: true });
    const notifications: QueueNotification[] = [];

    for (const agentEntry of agentEntries) {
      if (!agentEntry.isDirectory() || this.isIgnoredDirectory(agentEntry.name)) {
        continue;
      }

      const agentDir = join(inboxDir, agentEntry.name);
      // eslint-disable-next-line security/detect-non-literal-fs-filename
      const files = await readdir(agentDir, { withFileTypes: true });

      for (const fileEntry of files) {
        if (!fileEntry.isFile() || !fileEntry.name.endsWith(".json")) {
          continue;
        }

        const filePath = join(agentDir, fileEntry.name);
        const [rawPayload, fileStats] = await Promise.all([
          // eslint-disable-next-line security/detect-non-literal-fs-filename
          readFile(filePath, "utf8"),
          // eslint-disable-next-line security/detect-non-literal-fs-filename
          stat(filePath),
        ]);

        notifications.push({
          id: basename(fileEntry.name, ".json"),
          agent: agentEntry.name,
          filename: fileEntry.name,
          payload: JSON.parse(rawPayload) as unknown,
          createdAt: fileStats.birthtime,
        });
      }
    }

    return notifications.sort(
      (left, right) => right.createdAt.getTime() - left.createdAt.getTime()
    );
  }

  async streamNotifications(res: Response): Promise<void> {
    res.setHeader("Content-Type", "text/event-stream");
    res.setHeader("Cache-Control", "no-cache");
    res.setHeader("Connection", "keep-alive");
    res.setHeader("X-Accel-Buffering", "no");

    if (typeof res.flushHeaders === "function") {
      res.flushHeaders();
    }

    const emitNotifications = async (): Promise<void> => {
      try {
        const notifications = await this.listNotifications();
        res.write(`data: ${JSON.stringify(notifications)}\n\n`);
      } catch (error: unknown) {
        const message = error instanceof Error ? error.message : String(error);
        res.write(`event: error\n`);
        res.write(`data: ${JSON.stringify({ error: message })}\n\n`);
      }
    };

    await emitNotifications();

    const watcher = chokidar.watch(this.getInboxDir(), {
      ignoreInitial: true,
      persistent: true,
      ignored: (watchedPath: string) => {
        return watchedPath.includes("/_acked/") || watchedPath.includes("/_dead-letter/");
      },
    });

    watcher.on("add", () => {
      void emitNotifications();
    });

    watcher.on("unlink", () => {
      void emitNotifications();
    });

    res.on("close", () => {
      void watcher.close();
      res.end();
    });
  }

  async ackNotification(id: string): Promise<{ success: true; id: string }> {
    const notification = (await this.listNotifications()).find((entry) => entry.id === id);

    if (!notification) {
      throw new NotFoundException(`Queue notification ${id} not found`);
    }

    await this.execQueueCli(["ack", notification.id]);

    return {
      success: true,
      id: notification.id,
    };
  }

  async listTasks(): Promise<QueueTask[]> {
    const stdout = await this.execQueueCli(["list", "mosaic-stack"]);

    return stdout
      .split(/\r?\n/)
      .map((line) => line.trim())
      .filter((line) => line.length > 0)
      .map((line) => {
        const [rawId = "", projectTaskId = "", rawStatus = "", description = ""] = line.split("\t");
        const [project = "", taskId = ""] = projectTaskId.split("/");

        return {
          id: rawId,
          project,
          taskId,
          status: rawStatus.replace(/^\[/, "").replace(/\]$/, ""),
          description,
        };
      });
  }

  async notifyAgentCiResult(opts: {
    branch: string;
    status: "success" | "failure";
    buildUrl: string;
    repo: string;
  }): Promise<{ notified: number }> {
    const activeRecords = await this.listActiveAgentStateRecords();
    const matches = activeRecords.filter((record) => record.branch === opts.branch);

    await Promise.all(
      matches.map(async (record) => {
        const notification: AgentCiNotification = {
          id: randomUUID(),
          taskId: record.taskId,
          event: opts.status === "success" ? "completed" : "failed",
          targetAgent: record.agent,
          fromAgent: "mosaic-api",
          payload: {
            branch: opts.branch,
            buildUrl: opts.buildUrl,
            repo: opts.repo,
            ciStatus: opts.status,
          },
          createdAt: new Date().toISOString(),
          retries: 0,
          maxRetries: 3,
          ttlSeconds: 600,
        };

        const agentDir = join(this.getInboxDir(), record.agent);
        // Path is built from the configured inbox root plus validated agent-state entries.
        // eslint-disable-next-line security/detect-non-literal-fs-filename
        await mkdir(agentDir, { recursive: true });
        // Path is built from the configured inbox root plus validated agent-state entries.
        // eslint-disable-next-line security/detect-non-literal-fs-filename
        await writeFile(
          join(agentDir, `${notification.id}.json`),
          JSON.stringify(notification, null, 2),
          "utf8"
        );
      })
    );

    return { notified: matches.length };
  }

  private async execQueueCli(args: string[]): Promise<string> {
    const cliPath = this.getQueueCliPath();

    return new Promise<string>((resolve, reject) => {
      execFile("node", [cliPath, ...args], (error, stdout, stderr) => {
        if (error) {
          this.logger.error(
            `Queue CLI command failed: node ${cliPath} ${args.join(" ")} | ${stderr || error.message}`
          );
          reject(
            new InternalServerErrorException(`Queue CLI command failed: ${stderr || error.message}`)
          );
          return;
        }

        resolve(stdout);
      });
    });
  }

  private getInboxDir(): string {
    return this.expandHomePath(
      this.configService.get<string>("MOSAIC_QUEUE_INBOX_DIR") ??
        "~/.openclaw/workspace/agent-inbox"
    );
  }

  private getAgentStateDir(): string {
    return this.expandHomePath(
      this.configService.get<string>("MOSAIC_AGENT_STATE_DIR") ?? "~/.openclaw/workspace/agents"
    );
  }

  private getQueueCliPath(): string {
    return this.expandHomePath(
      this.configService.get<string>("MOSAIC_QUEUE_CLI") ?? "~/src/mosaic-queue/dist/cli.js"
    );
  }

  private expandHomePath(value: string): string {
    if (value === "~") {
      return homedir();
    }

    if (value.startsWith("~/")) {
      return join(homedir(), value.slice(2));
    }

    return value;
  }

  private async inboxDirExists(): Promise<boolean> {
    try {
      await access(this.getInboxDir());
      return true;
    } catch {
      return false;
    }
  }

  private isIgnoredDirectory(name: string): boolean {
    return name === "_acked" || name === "_dead-letter";
  }

  private async listActiveAgentStateRecords(): Promise<AgentStateRecord[]> {
    const activeDir = join(this.getAgentStateDir(), "active");

    try {
      // Path comes from controlled config and a fixed "active" subdirectory.
      // eslint-disable-next-line security/detect-non-literal-fs-filename
      const entries = await readdir(activeDir, { withFileTypes: true });
      const records = await Promise.all(
        entries
          .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
          .map(async (entry) => {
            const filePath = join(activeDir, entry.name);
            // Path comes from controlled config plus directory entries under the active root.
            // eslint-disable-next-line security/detect-non-literal-fs-filename
            const rawRecord = await readFile(filePath, "utf8");
            return JSON.parse(rawRecord) as AgentStateRecord;
          })
      );

      return records.filter((record) => {
        return (
          typeof record.taskId === "string" &&
          typeof record.agent === "string" &&
          typeof record.branch === "string"
        );
      });
    } catch (error: unknown) {
      const message = error instanceof Error ? error.message : String(error);
      this.logger.warn(`Unable to read active agent-state records from ${activeDir}: ${message}`);
      return [];
    }
  }
}
