import { createHash } from "node:crypto";
import { stat } from "node:fs/promises";
import type {
  PilotDeckToolRuntimeContext,
  PilotDeckWriteSnapshotEntry,
} from "../../protocol/types.js";
import { PilotDeckToolRuntimeError } from "../../protocol/errors.js";
import { readTextFile } from "./readTextFile.js";

export function getWriteSnapshot(
  context: PilotDeckToolRuntimeContext,
  absolutePath: string,
): PilotDeckWriteSnapshotEntry | undefined {
  return context.writeSnapshots?.get(absolutePath);
}

export function recordWriteSnapshot(
  context: PilotDeckToolRuntimeContext,
  absolutePath: string,
  content: string,
  mtimeMs: number,
  range?: { offset?: number; limit?: number },
): void {
  context.writeSnapshots ??= new Map();
  context.writeSnapshots.set(absolutePath, {
    absolutePath,
    mtimeMs: Math.floor(mtimeMs),
    contentHash: hashText(content),
    offset: range?.offset,
    limit: range?.limit,
  });
}

export function invalidateReadFileState(
  context: { readFileState?: Map<string, unknown> },
  absolutePath: string,
): void {
  if (!context.readFileState) return;
  const prefix = `${absolutePath}::`;
  for (const key of context.readFileState.keys()) {
    if (key.startsWith(prefix)) {
      context.readFileState.delete(key);
    }
  }
}

export async function validateWriteSnapshotFresh(
  context: PilotDeckToolRuntimeContext,
  absolutePath: string,
): Promise<{ exists: boolean }> {
  const fileStat = await stat(absolutePath).catch((error: unknown) => {
    if (isNodeError(error) && error.code === "ENOENT") {
      return undefined;
    }
    throw error;
  });

  if (!fileStat) {
    return { exists: false };
  }

  if (!fileStat.isFile()) {
    throw new PilotDeckToolRuntimeError("file_conflict", `${absolutePath} is not a regular file.`);
  }

  const snapshot = getWriteSnapshot(context, absolutePath);
  if (!snapshot) {
    throw new PilotDeckToolRuntimeError(
      "invalid_tool_input",
      "File has not been read yet. Read it first before writing to it.",
    );
  }

  const normalizedMtime = Math.floor(fileStat.mtimeMs);
  if (normalizedMtime === snapshot.mtimeMs) {
    return { exists: true };
  }

  const isFullRead = snapshot.offset === undefined && snapshot.limit === undefined;
  if (isFullRead) {
    const previousContent = await readTextFile(absolutePath);
    const currentHash = hashText(previousContent);
    if (currentHash === snapshot.contentHash) {
      return { exists: true };
    }
  }

  throw new PilotDeckToolRuntimeError(
    "invalid_tool_input",
    "File has changed since the last read. Read it again before writing to it.",
    {
      absolutePath,
      expectedMtimeMs: snapshot.mtimeMs,
      actualMtimeMs: normalizedMtime,
    },
  );
}

export async function ensureWriteSnapshotFresh(
  context: PilotDeckToolRuntimeContext,
  absolutePath: string,
): Promise<{ exists: boolean; previousContent: string | null; mtimeMs: number | null }> {
  const fileStat = await stat(absolutePath).catch((error: unknown) => {
    if (isNodeError(error) && error.code === "ENOENT") {
      return undefined;
    }
    throw error;
  });

  if (!fileStat) {
    return { exists: false, previousContent: null, mtimeMs: null };
  }

  if (!fileStat.isFile()) {
    throw new PilotDeckToolRuntimeError("file_conflict", `${absolutePath} is not a regular file.`);
  }

  const snapshot = getWriteSnapshot(context, absolutePath);
  if (!snapshot) {
    throw new PilotDeckToolRuntimeError(
      "invalid_tool_input",
      "File has not been read yet. Read it first before writing to it.",
    );
  }

  const normalizedMtime = Math.floor(fileStat.mtimeMs);
  const previousContent = await readTextFile(absolutePath);

  if (normalizedMtime !== snapshot.mtimeMs) {
    const isFullRead = snapshot.offset === undefined && snapshot.limit === undefined;
    if (isFullRead) {
      const currentHash = hashText(previousContent);
      if (currentHash === snapshot.contentHash) {
        return { exists: true, previousContent, mtimeMs: normalizedMtime };
      }
    }
    throw new PilotDeckToolRuntimeError(
      "invalid_tool_input",
      "File has changed since the last read. Read it again before writing to it.",
      {
        absolutePath,
        expectedMtimeMs: snapshot.mtimeMs,
        actualMtimeMs: normalizedMtime,
      },
    );
  }

  const currentHash = hashText(previousContent);
  if (currentHash !== snapshot.contentHash) {
    throw new PilotDeckToolRuntimeError(
      "invalid_tool_input",
      "File has changed since the last read. Read it again before writing to it.",
      {
        absolutePath,
        expectedMtimeMs: snapshot.mtimeMs,
        actualMtimeMs: normalizedMtime,
      },
    );
  }

  return { exists: true, previousContent, mtimeMs: normalizedMtime };
}

function hashText(value: string): string {
  return createHash("sha256").update(value, "utf8").digest("hex");
}

function isNodeError(error: unknown): error is NodeJS.ErrnoException {
  return error instanceof Error && "code" in error;
}