import * as fs from "node:fs";
import * as fsp from "node:fs/promises";
import * as path from "node:path";

const ANSI_REGEX = /\x1B\[[0-?]*[ -/]*[@-~]/g;

export type CycleStatus = "ok" | "error";
export type Scope =
  | "embeddable"
  | "dataModel"
  | "securityContext"
  | "clientContext"
  | "push"
  | "hmr";
export type IssueStage = "validate" | "sync";

export interface ValidationIssuePayload {
  scope: Scope;
  filePath: string;
  message: string;
  line?: number;
  column?: number;
  path?: string;
  stage?: IssueStage;
}

export interface DevLoggerInitOptions {
  logFile?: string;
  eventsFile?: string;
}

interface PatchedWrite {
  __devLoggerPatched?: true;
}

const stripAnsi = (input: unknown): string => {
  if (typeof input === "string") {
    return input.replaceAll(ANSI_REGEX, "");
  }
  if (input instanceof Uint8Array) {
    return Buffer.from(input).toString("utf8").replaceAll(ANSI_REGEX, "");
  }
  return "";
};

class DevLogger {
  private logStream?: fs.WriteStream;
  private eventsStream?: fs.WriteStream;
  private cycleCounter = 0;
  private originalStdoutWrite?: typeof process.stdout.write;
  private originalStderrWrite?: typeof process.stderr.write;

  async init(opts: DevLoggerInitOptions): Promise<void> {
    if (opts.logFile) {
      this.logStream = await this.openStream(opts.logFile);
      this.installMirror(this.logStream);
    }
    if (opts.eventsFile) {
      this.eventsStream = await this.openStream(opts.eventsFile);
    }
  }

  async close(): Promise<void> {
    this.uninstallMirror();
    await Promise.all([
      this.endStream(this.logStream),
      this.endStream(this.eventsStream),
    ]);
    this.logStream = undefined;
    this.eventsStream = undefined;
  }

  marker(event: string, payload: Record<string, unknown> = {}): void {
    this.writeEvent({ type: "marker", event, ...payload });
  }

  issue(payload: ValidationIssuePayload): void {
    if (!this.eventsStream) return;
    const { filePath, column, ...rest } = payload;
    const obj: Record<string, unknown> = {
      type: "issue",
      event: "validation_error",
      file: filePath,
      ...rest,
    };
    if (column !== undefined) obj.col = column;
    this.writeEvent(obj);
  }

  startCycle(scope: Scope, payload: Record<string, unknown> = {}): number {
    const cycle = ++this.cycleCounter;
    this.marker("validate_start", { cycle, scope, ...payload });
    return cycle;
  }

  endCycle(
    cycleId: number,
    scope: Scope,
    status: CycleStatus,
    payload: Record<string, unknown> = {},
  ): void {
    this.marker("validate_end", { cycle: cycleId, scope, status, ...payload });
  }

  private writeEvent(obj: Record<string, unknown>): void {
    if (!this.eventsStream) return;
    const line = JSON.stringify({ ts: new Date().toISOString(), ...obj });
    this.eventsStream.write(line + "\n");
  }

  private async openStream(filePath: string): Promise<fs.WriteStream> {
    const resolved = path.resolve(process.cwd(), filePath);
    await fsp.mkdir(path.dirname(resolved), { recursive: true });
    return fs.createWriteStream(resolved, { flags: "w" });
  }

  private installMirror(stream: fs.WriteStream): void {
    this.originalStdoutWrite = process.stdout.write;
    this.originalStderrWrite = process.stderr.write;

    const wrap =
      (orig: typeof process.stdout.write, target: NodeJS.WriteStream) =>
      (chunk: unknown, encoding?: unknown, cb?: unknown): boolean => {
        try {
          stream.write(stripAnsi(chunk));
        } catch {
          // never let mirror errors break the dev server
        }
        return (orig as (...args: unknown[]) => boolean).call(
          target,
          chunk,
          encoding,
          cb,
        );
      };

    const stdoutPatch = wrap(
      this.originalStdoutWrite,
      process.stdout,
    ) as typeof process.stdout.write & PatchedWrite;
    const stderrPatch = wrap(
      this.originalStderrWrite,
      process.stderr,
    ) as typeof process.stderr.write & PatchedWrite;
    stdoutPatch.__devLoggerPatched = true;
    stderrPatch.__devLoggerPatched = true;
    process.stdout.write = stdoutPatch;
    process.stderr.write = stderrPatch;
  }

  private uninstallMirror(): void {
    if (this.originalStdoutWrite) {
      process.stdout.write = this.originalStdoutWrite;
      this.originalStdoutWrite = undefined;
    }
    if (this.originalStderrWrite) {
      process.stderr.write = this.originalStderrWrite;
      this.originalStderrWrite = undefined;
    }
  }

  private endStream(stream?: fs.WriteStream): Promise<void> {
    if (!stream) return Promise.resolve();
    return new Promise((resolve) => {
      stream.end(() => resolve());
    });
  }
}

const devLogger = new DevLogger();
export default devLogger;
export { stripAnsi };
