import { randomUUID } from "node:crypto";
import { chmod, lstat, mkdir, readdir, realpath, rm } from "node:fs/promises";
import { isAbsolute, join, resolve } from "node:path";
import {
  linkWorkspaceFileNoFollow,
  removeWorkspaceFileIfUnchangedNoFollow,
  type WorkspaceFileWriteResult,
} from "../security/workspaceFiles.js";
import { downloadMessageFile, type FeishuRuntime } from "./client.js";

export interface MessageResourceStageRequest {
  readonly fileKey: string;
  readonly resourceType: "image" | "file";
  readonly workspaceRelativePath: string;
}

export interface StagedMessageResource {
  readonly resourceType: "image" | "file";
  readonly workspaceRelativePath: string;
  readonly stagedPath: string;
}

export interface StagedMessageResourceBatch {
  readonly stagingRoot: string;
  readonly resources: readonly StagedMessageResource[];
  readonly publishedResources: PublishedMessageResource[];
}

export interface PublishedMessageResource extends WorkspaceFileWriteResult {
  readonly resourceType: "image" | "file";
  readonly workspaceRelativePath: string;
}

/** Download Feishu resources into a private temporary workspace, never the tenant workspace. */
export async function stageMessageResources(
  rt: FeishuRuntime,
  messageId: string,
  requests: readonly MessageResourceStageRequest[],
  workspaceRoot: string,
  limits?: { readonly maxFiles: number; readonly maxBytesPerFile: number },
): Promise<StagedMessageResourceBatch | null> {
  if (requests.length === 0) return null;
  if (limits !== undefined && requests.length > limits.maxFiles) {
    throw new Error(`Feishu message has ${requests.length} resources; limit is ${limits.maxFiles}`);
  }
  const stagingBase = await requirePrivateStagingBase(workspaceRoot);
  const stagingRoot = join(stagingBase, randomUUID());
  const resources: StagedMessageResource[] = [];
  try {
    await mkdir(stagingRoot, { mode: 0o700 });
    for (const [index, request] of requests.entries()) {
      const stagedPath = await downloadMessageFile(
        rt,
        messageId,
        request.fileKey,
        workspaceRoot,
        stagingRoot,
        `resource-${index}`,
        request.resourceType,
        limits?.maxBytesPerFile,
      );
      resources.push({
        resourceType: request.resourceType,
        workspaceRelativePath: request.workspaceRelativePath,
        stagedPath,
      });
    }
    return { stagingRoot, resources, publishedResources: [] };
  } catch (error) {
    try {
      await rm(stagingRoot, { recursive: true, force: true });
    } catch (cleanupError) {
      throw new AggregateError(
        [error, cleanupError],
        `Feishu resource staging and cleanup both failed: ${stagingRoot}`,
        { cause: error },
      );
    }
    throw error;
  }
}

/**
 * Publish with O(1) same-filesystem links only after ACTIVE run/project-lock
 * admission has committed durably; that commit is the lifecycle linearization point.
 */
export async function publishStagedMessageResources(
  batch: StagedMessageResourceBatch,
  workspaceRoot: string,
  workspaceDir: string,
): Promise<readonly PublishedMessageResource[]> {
  for (const resource of batch.resources) {
    const result = await linkWorkspaceFileNoFollow(
      workspaceRoot,
      workspaceDir,
      resource.workspaceRelativePath,
      resource.stagedPath,
    );
    batch.publishedResources.push({
      ...result,
      resourceType: resource.resourceType,
      workspaceRelativePath: resource.workspaceRelativePath,
    });
  }
  return batch.publishedResources;
}

export async function removeStagedMessageResources(batch: StagedMessageResourceBatch): Promise<void> {
  await rm(batch.stagingRoot, { recursive: true, force: true });
}

/** No trigger can be live during startup, so every prior staging batch is abandoned. */
export async function removeAbandonedMessageResourceStages(workspaceRoot: string): Promise<number> {
  const canonicalRoot = await canonicalWorkspaceRoot(workspaceRoot, true);
  if (canonicalRoot === null) return 0;
  const stagingBase = join(canonicalRoot, ".cph-staging");
  let metadata;
  try {
    metadata = await lstat(stagingBase);
  } catch (error) {
    if (isErrno(error, "ENOENT")) return 0;
    throw error;
  }
  if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
    throw new Error(`Feishu staging base is not a real directory: ${stagingBase}`);
  }
  const entries = await readdir(stagingBase);
  const removed = await Promise.allSettled(entries.map((entry) =>
    rm(join(stagingBase, entry), { recursive: true, force: true })
  ));
  const failures = removed.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
  if (failures.length > 0) {
    throw new AggregateError(failures, `failed to remove abandoned Feishu staging batches: ${stagingBase}`);
  }
  return entries.length;
}

export async function compensatePublishedMessageResources(
  resources: readonly PublishedMessageResource[],
  workspaceRoot: string,
  workspaceDir: string,
): Promise<void> {
  const results = await Promise.allSettled(resources.map((resource) =>
    removeWorkspaceFileIfUnchangedNoFollow(
      workspaceRoot,
      workspaceDir,
      resource.workspaceRelativePath,
      resource.device,
      resource.inode,
    )
  ));
  const failures = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
  if (failures.length > 0) {
    throw new AggregateError(failures, "failed to compensate one or more published Feishu resources");
  }
}

async function requirePrivateStagingBase(workspaceRoot: string): Promise<string> {
  const canonicalRoot = await canonicalWorkspaceRoot(workspaceRoot, false);
  if (canonicalRoot === null) throw new Error(`workspace root not found: ${workspaceRoot}`);
  const stagingBase = join(canonicalRoot, ".cph-staging");
  try {
    await mkdir(stagingBase, { mode: 0o700 });
  } catch (error) {
    if (!isErrno(error, "EEXIST")) throw error;
  }
  const metadata = await lstat(stagingBase);
  if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
    throw new Error(`Feishu staging base is not a real directory: ${stagingBase}`);
  }
  const canonicalStagingBase = await realpath(stagingBase);
  const expectedStagingBase = join(await realpath(canonicalRoot), ".cph-staging");
  if (canonicalStagingBase !== expectedStagingBase) {
    throw new Error(`Feishu staging base escapes workspace root: ${stagingBase}`);
  }
  await chmod(stagingBase, 0o700);
  return stagingBase;
}

async function canonicalWorkspaceRoot(
  workspaceRoot: string,
  allowMissing: boolean,
): Promise<string | null> {
  if (!isAbsolute(workspaceRoot)) throw new Error(`workspace root must be absolute: ${workspaceRoot}`);
  const configuredRoot = resolve(workspaceRoot);
  let metadata;
  try {
    metadata = await lstat(configuredRoot);
  } catch (error) {
    if (allowMissing && isErrno(error, "ENOENT")) return null;
    throw error;
  }
  if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
    throw new Error(`workspace root is not a real directory: ${workspaceRoot}`);
  }
  await realpath(configuredRoot);
  return configuredRoot;
}

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