/**
 * Workspace file tools - bounded read/write/list against the project's
 * curriculum engineering-file tree (ADR-0007 directory tree).
 *
 * All paths are **confined to `ctx.workspaceDir`**: the handler resolves the
 * requested relative path against the workspace root and rejects any path that
 * escapes it (via `..` or absolute paths). This is the agent's only file
 * surface; the Hub agent is narrower than a general coding agent.
 */
import { readFile, writeFile, readdir, mkdir } from "node:fs/promises";
import { createHash } from "node:crypto";
import { join, resolve, relative, isAbsolute } from "node:path";
import { tool } from "ai";
import { z } from "zod";
import type { ToolContext, ToolDefinition } from "./tools.js";

/** Thrown when a requested path escapes the project workspace root. */
export class PathEscape extends Error {
  constructor(readonly requested: string, readonly workspaceDir: string) {
    super(`path escapes workspace: ${requested} (root ${workspaceDir})`);
    this.name = "PathEscape";
  }
}

/** Resolve a tool-supplied path against the workspace root, rejecting escapes. */
function confine(requestedPath: string, workspaceDir: string): string {
  if (isAbsolute(requestedPath)) {
    // Allow absolute paths only if they're already inside the workspace.
    const rel = relative(workspaceDir, requestedPath);
    if (rel.startsWith("..") || rel === "") {
      throw new PathEscape(requestedPath, workspaceDir);
    }
    return requestedPath;
  }
  const resolved = resolve(workspaceDir, requestedPath);
  const rel = relative(workspaceDir, resolved);
  if (rel.startsWith("..")) {
    throw new PathEscape(requestedPath, workspaceDir);
  }
  return resolved;
}

export function readFileTool(ctx: ToolContext): ToolDefinition {
  return tool({
    description: "Read a file from the project's curriculum engineering-file workspace. Path is relative to the workspace root.",
    inputSchema: z.object({
      path: z.string().describe("Relative path within the workspace."),
    }),
    execute: async (args): Promise<string> => {
      try {
        const full = confine(args.path, ctx.workspaceDir);
        return await readFile(full, "utf8");
      } catch (e) {
        return JSON.stringify({ error: e instanceof Error ? e.message : String(e) });
      }
    },
  });
}

export function writeFileTool(ctx: ToolContext): ToolDefinition {
  return tool({
    description:
      "Write a file in the project's curriculum engineering-file workspace. Path is relative to the workspace root. Creates parent directories.",
    inputSchema: z.object({
      path: z.string().describe("Relative path within the workspace."),
      content: z.string().describe("The full file content to write."),
    }),
    execute: async (args): Promise<string> => {
      try {
        const full = confine(args.path, ctx.workspaceDir);
        // A-3 file-change capture: compute before/after hashes around the write.
        // A hashing failure must not block the write — skip the sink in that case.
        let beforeHash: string | null = null;
        let afterHash: string | null = null;
        let operation: "CREATE" | "UPDATE" | null = null;
        try {
          let oldContent: string | null;
          try {
            oldContent = await readFile(full, "utf8");
          } catch (e) {
            if (e !== null && typeof e === "object" && "code" in e && e.code === "ENOENT") {
              oldContent = null;
            } else {
              throw e;
            }
          }
          afterHash = createHash("sha256").update(args.content).digest("hex");
          beforeHash = oldContent === null ? null : createHash("sha256").update(oldContent).digest("hex");
          if (beforeHash === null) {
            operation = "CREATE";
          } else if (beforeHash === afterHash) {
            operation = null;
          } else {
            operation = "UPDATE";
          }
        } catch {
          operation = null;
        }
        await mkdir(join(full, ".."), { recursive: true });
        await writeFile(full, args.content, "utf8");
        if (operation !== null && ctx.onFileChange !== undefined) {
          ctx.onFileChange({ path: args.path, operation, beforeHash, afterHash, diff: null });
        }
        return JSON.stringify({ ok: true, path: args.path, bytes: args.content.length });
      } catch (e) {
        return JSON.stringify({ error: e instanceof Error ? e.message : String(e) });
      }
    },
  });
}

interface Entry {
  readonly name: string;
  readonly kind: "file" | "dir";
}

export function listFilesTool(ctx: ToolContext): ToolDefinition {
  return tool({
    description: "List files and directories at a path within the workspace. Defaults to the workspace root.",
    inputSchema: z.object({
      path: z.string().describe("Relative path within the workspace; defaults to root.").optional(),
    }),
    execute: async (args): Promise<string> => {
      const sub = args.path ?? ".";
      try {
        const full = confine(sub, ctx.workspaceDir);
        const entries = await readdir(full, { withFileTypes: true });
        const result: Entry[] = entries.map((e) => ({
          name: e.name,
          kind: e.isDirectory() ? "dir" : "file",
        }));
        return JSON.stringify(result);
      } catch (e) {
        return JSON.stringify({ error: e instanceof Error ? e.message : String(e) });
      }
    },
  });
}
