/**
 * ADR-0027: pdf_to_md_bundle capability adapter.
 *
 * Converts a PDF in the run's workspace into a Markdown bundle (md + extracted
 * images) by calling the MinerU document parsing service, writing outputs into
 * the workspace, and recording consumption on a UsageFact (ADR-0026).
 *
 * Invariants (ADR-0027):
 *   1. Credential isolation — the capability credential is resolved in Hub and
 *      never reaches the Agent process. The MineruClient receives it as a
 *      call argument, not from the environment.
 *   2. Workspace containment — input and output paths are confined to the
 *      run's workspace dir (ADR-0018 AgentSurface). Escapes are rejected.
 *   3. Mandatory fact — a successful invocation always writes ≥1 UsageFact
 *      with kind=external_capability, even when costUsd is null (ADR-0022:
 *      missing cost ≠ zero).
 */
import { mkdir, writeFile } from "node:fs/promises";
import { join, resolve, relative, isAbsolute } from "node:path";
import type { PrismaClient } from "@prisma/client";
import { LocalSecretEnvelope } from "../security/secretEnvelope.js";
import { resolveCapabilityCredential } from "./capabilityConnections.js";
import { DocmindClientError, type CapabilityProviderClient } from "./docmindClient.js";
import {
  CAPABILITIES,
  type CapabilityAdapter,
  type CapabilityInvocationInput,
  type CapabilityInvocationResult,
  type CapabilityOutputArtifact,
} from "./types.js";

const CAPABILITY_ID = "pdf_to_md_bundle" as const;
const PROVIDER_ID = "aliyun_docmind";

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

/** Resolve a workspace-relative path, rejecting escapes (ADR-0018 AgentSurface). */
function confineToWorkspace(requestedPath: string, workspaceDir: string): string {
  if (isAbsolute(requestedPath)) {
    const rel = relative(workspaceDir, requestedPath);
    if (rel.startsWith("..") || rel === "") {
      throw new CapabilityPathEscape(requestedPath, workspaceDir);
    }
    return requestedPath;
  }
  const resolved = resolve(workspaceDir, requestedPath);
  const rel = relative(workspaceDir, resolved);
  if (rel.startsWith("..")) {
    throw new CapabilityPathEscape(requestedPath, workspaceDir);
  }
  return resolved;
}

export interface PdfToMdBundleDeps {
  readonly secrets: LocalSecretEnvelope;
  readonly client: CapabilityProviderClient;
  readonly prisma: PrismaClient;
}

/** Build the pdf_to_md_bundle adapter. The client is injectable for testing. */
export function createPdfToMdBundleAdapter(deps: PdfToMdBundleDeps): CapabilityAdapter {
  return {
    capabilityId: CAPABILITY_ID,
    async invoke(input: CapabilityInvocationInput): Promise<CapabilityInvocationResult> {
      const descriptor = CAPABILITIES[CAPABILITY_ID];
      // 1. Resolve org-scoped credential (fail-closed, ADR-0024/0027).
      const credential = await resolveCapabilityCredential(deps.prisma, deps.secrets, {
        organizationId: input.organizationId,
        capabilityId: CAPABILITY_ID,
      });

      // 2. Confine input + output paths to the workspace (ADR-0018).
      const absoluteInput = confineToWorkspace(input.inputPath, input.workspaceDir);
      const absoluteOutputDir = confineToWorkspace(input.outputDir, input.workspaceDir);
      await mkdir(absoluteOutputDir, { recursive: true });

      // 3. Call the backing service.
      let result;
      try {
        result = await deps.client.parse(credential, { inputFilePath: absoluteInput });
      } catch (e) {
        if (e instanceof DocmindClientError) throw e;
        throw new DocmindClientError(
          e instanceof Error ? e.message : String(e),
          "docmind_unreachable",
        );
      }
      if (result.markdown === "") {
        throw new DocmindClientError("docmind returned empty markdown", "docmind_no_output");
      }

      // 4. Write outputs into the workspace.
      const artifacts: CapabilityOutputArtifact[] = [];
      const mdPath = join(absoluteOutputDir, "document.md");
      await writeFile(mdPath, result.markdown, "utf8");
      artifacts.push({ path: relative(input.workspaceDir, mdPath), kind: "markdown" });

      for (const image of result.images) {
        const imagePath = join(absoluteOutputDir, image.filename);
        const rel = relative(absoluteOutputDir, imagePath);
        if (rel.startsWith("..")) {
          // Defensive: image filename must not escape the output dir.
          throw new CapabilityPathEscape(image.filename, absoluteOutputDir);
        }
        await writeFile(imagePath, image.data);
        artifacts.push({ path: relative(input.workspaceDir, imagePath), kind: "image" });
      }

      // 5. Write the UsageFact (ADR-0026/0027). Always written on success;
      //    costUsd null means unknown, NOT zero (ADR-0022).
      const occurredAt = new Date();
      await deps.prisma.usageFact.create({
        data: {
          runId: input.runId,
          occurredAt,
          kind: "external_capability",
          provider: PROVIDER_ID,
          model: null,
          inputTokens: null,
          outputTokens: null,
          quantity: result.pageCount,
          unit: descriptor.meteringUnit,
          costUsd: result.costUsd,
          costSource: result.costUsd !== null ? "provider_reported" : "unknown",
          capabilityId: CAPABILITY_ID,
          correlationId: result.requestId,
          metadata: {},
        },
      });

      return {
        artifacts,
        consumption: {
          provider: PROVIDER_ID,
          model: null,
          inputTokens: null,
          outputTokens: null,
          quantity: result.pageCount,
          unit: descriptor.meteringUnit,
          costUsd: result.costUsd,
          correlationId: result.requestId,
        },
      };
    },
  };
}
