import { createHash } from "node:crypto";
import { readFileSync, statSync } from "node:fs";
import type { ThreadId, ThreadWorkspaceStats } from "@best-agent/protocol";
import { BestAgentError } from "@best-agent/foundation";

type WorkspaceSnapshotSource = "read" | "write";

type WorkspaceSnapshot = {
  contentHash: string | null;
  exists: boolean;
  isFullRead: boolean;
  mtimeMs: number | null;
  revision: number;
  size: number | null;
  source: WorkspaceSnapshotSource;
};

type WorkspaceFileState = {
  contentHash: string | null;
  exists: boolean;
  mtimeMs: number | null;
  size: number | null;
};

export interface WorkspaceMutationPromotion {
  invalidatedThreadIds: ThreadId[];
  revision: number;
}

export interface WorkspaceStaleViewErrorMetadata {
  currentRevision: number;
  path: string;
  reason: string;
  seenRevision?: number;
}

type WorkspaceStaleViewError = BestAgentError & {
  workspace: WorkspaceStaleViewErrorMetadata;
};

export class WorkspaceState {
  readonly #diagnostics = new Map<ThreadId, ThreadWorkspaceStats>();
  readonly #fileStates = new Map<string, WorkspaceFileState>();
  readonly #revisions = new Map<string, number>();
  readonly #snapshots = new Map<ThreadId, Map<string, WorkspaceSnapshot>>();
  readonly #writeLocks = new Map<string, Promise<void>>();

  recordRead(threadId: ThreadId, path: string): void {
    const currentRevision = this.#revisions.get(path) ?? 0;
    this.#getSnapshots(threadId).set(path, this.#readWorkspaceSnapshot(path, "read", currentRevision));
    this.#ensureDiagnostics(threadId).fullReadCount += 1;
  }

  assertMutationAllowed(threadId: ThreadId, path: string): void {
    const snapshots = this.#snapshots.get(threadId);
    const snapshot = snapshots?.get(path);
    const currentRevision = this.#revisions.get(path) ?? 0;
    const state = this.#readWorkspaceFileState(path);

    if (!state.exists) {
      if (!snapshot || !snapshot.exists) {
        return;
      }
      throw this.#createStaleViewError(threadId, {
        currentRevision,
        path,
        reason: "Path no longer exists. Re-read it before editing.",
        seenRevision: snapshot.revision,
      });
    }

    if (!snapshot) {
      throw this.#createStaleViewError(threadId, {
        currentRevision,
        path,
        reason: "Existing files must be read before they are modified.",
      });
    }

    if (!snapshot.isFullRead) {
      throw this.#createStaleViewError(threadId, {
        currentRevision,
        path,
        reason: "Existing files require a full read before they are modified.",
        seenRevision: snapshot.revision,
      });
    }

    if (snapshot.revision !== currentRevision) {
      throw this.#createStaleViewError(threadId, {
        currentRevision,
        path,
        reason: "Path changed after it was last read. Re-read it before editing.",
        seenRevision: snapshot.revision,
      });
    }

    if (
      snapshot.mtimeMs !== state.mtimeMs ||
      snapshot.size !== state.size ||
      snapshot.contentHash !== state.contentHash
    ) {
      throw this.#createStaleViewError(threadId, {
        currentRevision,
        path,
        reason: "Path changed on disk after it was last read. Re-read it before editing.",
        seenRevision: snapshot.revision,
      });
    }
  }

  promoteWrite(threadId: ThreadId, path: string): WorkspaceMutationPromotion {
    const nextRevision = (this.#revisions.get(path) ?? 0) + 1;
    this.#revisions.set(path, nextRevision);

    const invalidatedThreadIds: ThreadId[] = [];
    for (const [candidateThreadId, snapshots] of this.#snapshots.entries()) {
      if (candidateThreadId === threadId) {
        continue;
      }
      if (snapshots.delete(path)) {
        invalidatedThreadIds.push(candidateThreadId);
        this.#ensureDiagnostics(candidateThreadId).invalidationCount += 1;
      }
    }

    this.#fileStates.delete(path);
    this.#getSnapshots(threadId).set(path, this.#readWorkspaceSnapshot(path, "write", nextRevision));
    this.#ensureDiagnostics(threadId).promotedWriteCount += 1;

    return {
      invalidatedThreadIds,
      revision: nextRevision,
    };
  }

  async withWriteLocks<T>(paths: string[], action: () => Promise<T>): Promise<T> {
    const normalizedPaths = Array.from(new Set(paths.map((path) => path.trim()).filter(Boolean))).sort();
    if (normalizedPaths.length === 0) {
      return action();
    }

    const releases: Array<() => void> = [];
    try {
      for (const path of normalizedPaths) {
        releases.push(await this.#acquireWriteLock(path));
      }
      return await action();
    } finally {
      for (const release of releases.reverse()) {
        release();
      }
    }
  }

  getDiagnostics(threadId: ThreadId): ThreadWorkspaceStats {
    const diagnostics = this.#diagnostics.get(threadId);
    return {
      fullReadCount: diagnostics?.fullReadCount ?? 0,
      invalidationCount: diagnostics?.invalidationCount ?? 0,
      promotedWriteCount: diagnostics?.promotedWriteCount ?? 0,
      staleWriteRejectedCount: diagnostics?.staleWriteRejectedCount ?? 0,
    };
  }

  #createStaleViewError(threadId: ThreadId, metadata: WorkspaceStaleViewErrorMetadata): WorkspaceStaleViewError {
    this.#ensureDiagnostics(threadId).staleWriteRejectedCount += 1;
    const error = new BestAgentError("WORKSPACE_STALE_VIEW", `Workspace path ${metadata.path} is stale. ${metadata.reason}`);
    return Object.assign(error, { workspace: metadata });
  }

  async #acquireWriteLock(path: string): Promise<() => void> {
    const previous = this.#writeLocks.get(path) ?? Promise.resolve();
    let resolveCurrent!: () => void;
    const current = new Promise<void>((resolve) => {
      resolveCurrent = resolve;
    });
    this.#writeLocks.set(path, previous.then(() => current));
    await previous;
    let released = false;
    return () => {
      if (released) {
        return;
      }
      released = true;
      resolveCurrent();
      if (this.#writeLocks.get(path) === current) {
        this.#writeLocks.delete(path);
      }
    };
  }

  #ensureDiagnostics(threadId: ThreadId): ThreadWorkspaceStats {
    let diagnostics = this.#diagnostics.get(threadId);
    if (!diagnostics) {
      diagnostics = {
        fullReadCount: 0,
        invalidationCount: 0,
        promotedWriteCount: 0,
        staleWriteRejectedCount: 0,
      };
      this.#diagnostics.set(threadId, diagnostics);
    }
    return diagnostics;
  }

  #getSnapshots(threadId: ThreadId): Map<string, WorkspaceSnapshot> {
    let snapshots = this.#snapshots.get(threadId);
    if (!snapshots) {
      snapshots = new Map();
      this.#snapshots.set(threadId, snapshots);
    }
    return snapshots;
  }

  #readWorkspaceSnapshot(path: string, source: WorkspaceSnapshotSource, revision: number): WorkspaceSnapshot {
    const state = this.#readWorkspaceFileState(path);
    return {
      contentHash: state.contentHash,
      exists: state.exists,
      isFullRead: true,
      mtimeMs: state.mtimeMs,
      revision,
      size: state.size,
      source,
    };
  }

  #readWorkspaceFileState(path: string): WorkspaceFileState {
    try {
      const stat = statSync(path);
      if (!stat.isFile()) {
        this.#fileStates.delete(path);
        return {
          contentHash: null,
          exists: false,
          mtimeMs: null,
          size: null,
        };
      }

      const cached = this.#fileStates.get(path);
      if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
        return cached;
      }

      const content = readFileSync(path);
      const nextState = {
        contentHash: createHash("sha1").update(content).digest("hex"),
        exists: true,
        mtimeMs: stat.mtimeMs,
        size: stat.size,
      } satisfies WorkspaceFileState;
      this.#fileStates.set(path, nextState);
      return nextState;
    } catch {
      this.#fileStates.delete(path);
      return {
        contentHash: null,
        exists: false,
        mtimeMs: null,
        size: null,
      };
    }
  }
}

export function getWorkspaceStaleViewErrorMetadata(error: unknown): WorkspaceStaleViewErrorMetadata | undefined {
  if (!(error instanceof BestAgentError) || error.code !== "WORKSPACE_STALE_VIEW") {
    return undefined;
  }
  return (error as Partial<WorkspaceStaleViewError>).workspace;
}
