// Media reference helpers resolve media refs to file, URL, or inline payloads.
import fs from "node:fs/promises";
import path from "node:path";
import { hasHttpUrlPrefix } from "@openclaw/net-policy/url-protocol";
import { safeFileURLToPath } from "../infra/local-file-access.js";
import { resolveUserPath } from "../utils.js";
import { getMediaDir, resolveMediaBufferPath } from "./store.js";

type MediaReferenceErrorCode = "invalid-path" | "path-not-allowed";

/** Error raised when a media reference cannot be mapped to an allowed local media file. */
export class MediaReferenceError extends Error {
  code: MediaReferenceErrorCode;

  constructor(code: MediaReferenceErrorCode, message: string, options?: ErrorOptions) {
    super(message, options);
    this.code = code;
    this.name = "MediaReferenceError";
  }
}

type InboundMediaReference = {
  id: string;
  normalizedSource: string;
  physicalPath: string;
  sourceType: "uri" | "path";
};

type InboundMediaUri = {
  id: string;
  normalizedSource: string;
};

/** Strips legacy MEDIA: prefixes while preserving canonical media:// references. */
export function normalizeMediaReferenceSource(source: string): string {
  const trimmed = source.trim();
  if (/^media:\/\//i.test(trimmed)) {
    return trimmed;
  }
  return trimmed.replace(/^\s*MEDIA\s*:\s*/i, "").trim();
}

type MediaReferenceSourceInfo = {
  hasScheme: boolean;
  hasUnsupportedScheme: boolean;
  isDataUrl: boolean;
  isFileUrl: boolean;
  isHttpUrl: boolean;
  isMediaStoreUrl: boolean;
  looksLikeWindowsDrivePath: boolean;
};

/** Classifies media reference schemes before local resolution or sandbox rewriting. */
export function classifyMediaReferenceSource(
  source: string,
  options?: { allowDataUrl?: boolean },
): MediaReferenceSourceInfo {
  const allowDataUrl = options?.allowDataUrl ?? true;
  const looksLikeWindowsDrivePath = /^[a-zA-Z]:[\\/]/.test(source);
  const hasScheme = /^[a-z][a-z0-9+.-]*:/i.test(source);
  const isFileUrl = /^file:/i.test(source);
  const isHttpUrl = hasHttpUrlPrefix(source);
  const isDataUrl = /^data:/i.test(source);
  const isMediaStoreUrl = /^media:\/\//i.test(source);
  const hasUnsupportedScheme =
    hasScheme &&
    !looksLikeWindowsDrivePath &&
    !isFileUrl &&
    !isHttpUrl &&
    !isMediaStoreUrl &&
    !(allowDataUrl && isDataUrl);
  return {
    hasScheme,
    hasUnsupportedScheme,
    isDataUrl,
    isFileUrl,
    isHttpUrl,
    isMediaStoreUrl,
    looksLikeWindowsDrivePath,
  };
}

function maybeLocalPathFromSource(source: string): string | null {
  if (/^file:/i.test(source)) {
    try {
      return safeFileURLToPath(source);
    } catch {
      return null;
    }
  }
  if (source.startsWith("~")) {
    return resolveUserPath(source);
  }
  if (path.isAbsolute(source)) {
    return source;
  }
  return null;
}

function relativePathEscapesBase(relativePath: string): boolean {
  return (
    relativePath === ".." ||
    relativePath.startsWith("../") ||
    relativePath.startsWith("..\\") ||
    path.isAbsolute(relativePath)
  );
}

async function resolvePathForContainment(candidate: string): Promise<string> {
  try {
    return await fs.realpath(candidate);
  } catch {
    return path.resolve(candidate);
  }
}

/** Parses canonical inbound media-store URIs and rejects nested or cross-bucket references. */
export function parseInboundMediaUri(source: string): InboundMediaUri | null {
  const normalizedSource = normalizeMediaReferenceSource(source);
  if (!/^media:\/\//i.test(normalizedSource)) {
    return null;
  }

  let parsed: URL;
  try {
    parsed = new URL(normalizedSource);
  } catch (err) {
    throw new MediaReferenceError("invalid-path", `Invalid media URI: ${normalizedSource}`, {
      cause: err,
    });
  }

  if (parsed.hostname !== "inbound") {
    throw new MediaReferenceError(
      "path-not-allowed",
      `Unsupported media URI location: ${parsed.hostname || "(missing)"}`,
    );
  }

  let id: string;
  try {
    id = decodeURIComponent(parsed.pathname.replace(/^\/+/, ""));
  } catch (err) {
    throw new MediaReferenceError("invalid-path", `Invalid media URI: ${normalizedSource}`, {
      cause: err,
    });
  }

  if (!id || id.includes("/") || id.includes("\\") || id.includes("\0")) {
    throw new MediaReferenceError("invalid-path", `Invalid media URI: ${normalizedSource}`);
  }

  return {
    id,
    normalizedSource,
  };
}

async function resolveInboundMediaUri(
  normalizedSource: string,
): Promise<InboundMediaReference | null> {
  const uri = parseInboundMediaUri(normalizedSource);
  if (!uri) {
    return null;
  }
  return {
    ...uri,
    physicalPath: await resolveInboundMediaPath(uri.id, uri.normalizedSource),
    sourceType: "uri",
  };
}

/** Rewrites inbound media-store URIs to sandbox-relative paths for staged agent inputs. */
export function resolveMediaReferenceSandboxPath(
  source: string,
  inboundDir = "media/inbound",
): { resolved: string; rewrittenFrom?: string } {
  const normalizedSource = normalizeMediaReferenceSource(source);
  const uri = parseInboundMediaUri(normalizedSource);
  if (!uri) {
    return { resolved: normalizedSource };
  }
  return {
    resolved: path.posix.join(inboundDir.replace(/\\/g, "/"), uri.id),
    rewrittenFrom: uri.normalizedSource,
  };
}

/** Resolves inbound media:// URIs or first-level inbound file paths to concrete store files. */
export async function resolveInboundMediaReference(
  source: string,
): Promise<InboundMediaReference | null> {
  const normalizedSource = normalizeMediaReferenceSource(source);
  if (!normalizedSource) {
    return null;
  }

  const uriSource = await resolveInboundMediaUri(normalizedSource);
  if (uriSource) {
    return uriSource;
  }

  const localPath = maybeLocalPathFromSource(normalizedSource);
  if (!localPath) {
    return null;
  }

  const rawInboundDir = path.resolve(getMediaDir(), "inbound");
  const rawResolvedPath = path.resolve(localPath);
  const rawRel = path.relative(rawInboundDir, rawResolvedPath);
  // Realpath fallback catches symlinks and moved state dirs before accepting direct paths.
  const rel =
    rawRel && !relativePathEscapesBase(rawRel)
      ? rawRel
      : path.relative(
          await resolvePathForContainment(rawInboundDir),
          await resolvePathForContainment(localPath),
        );
  if (!rel || relativePathEscapesBase(rel) || rel.includes(path.sep)) {
    return null;
  }

  return {
    id: rel,
    normalizedSource,
    physicalPath: await resolveInboundMediaPath(rel, normalizedSource),
    sourceType: "path",
  };
}

/** Converts inbound media references for callers that need a direct local file path. */
export async function resolveMediaReferenceLocalPath(source: string): Promise<string> {
  const normalizedSource = normalizeMediaReferenceSource(source);
  return (await resolveInboundMediaReference(normalizedSource))?.physicalPath ?? normalizedSource;
}

async function resolveInboundMediaPath(id: string, source: string): Promise<string> {
  try {
    return await resolveMediaBufferPath(id, "inbound");
  } catch (err) {
    throw new MediaReferenceError(
      "invalid-path",
      err instanceof Error ? err.message : `Invalid media reference: ${source}`,
      { cause: err },
    );
  }
}
