import { json } from "@remix-run/server-runtime";
import { type IOPacket } from "@trigger.dev/core/v3";
import { env } from "~/env.server";
import { type AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { ServiceValidationError } from "~/v3/services/common.server";
import { singleton } from "~/utils/singleton";
import {
  normalizeObjectStoreLogicalKeyPathname,
  ObjectStoreClient,
  type ObjectStoreClientConfig,
} from "./objectStoreClient.server";

/**
 * Parsed storage URI with optional protocol prefix
 * @example { protocol: "s3", path: "run_abc/payload.json" }
 * @example { protocol: undefined, path: "batch_123/item_0/payload.json" } // legacy, uses default
 */
export type ParsedStorageUri = {
  protocol?: string;
  path: string;
};

/**
 * Parse a storage URI into protocol and path components
 * @param uri Storage URI, optionally prefixed with protocol (e.g., "s3://path" or "path")
 * @returns Parsed components { protocol?, path }
 */
export function parseStorageUri(uri: string): ParsedStorageUri {
  const match = uri.match(/^([a-z0-9]+):\/\/(.+)$/);
  if (match) {
    return {
      protocol: match[1],
      path: match[2],
    };
  }
  return {
    protocol: undefined,
    path: uri,
  };
}

/**
 * Format a storage URI with optional protocol prefix
 * @param path Storage path
 * @param protocol Optional protocol to prefix (e.g., "s3", "r2")
 * @returns Formatted URI (e.g., "s3://path" or "path")
 */
export function formatStorageUri(path: string, protocol?: string): string {
  if (protocol) {
    return `${protocol}://${path}`;
  }
  return path;
}

export const INVALID_PACKET_STORAGE_PATH = "Invalid packet storage path";

export type PacketPresignFailure = {
  success: false;
  error: string;
  status?: number;
};

const PACKET_RELATIVE_PATH_BASE = "/__packet_base__";

function throwInvalidPacketStoragePath(): never {
  throw new ServiceValidationError(INVALID_PACKET_STORAGE_PATH, 400);
}

function assertRawPacketRelativePathSegments(path: string): void {
  if (!path || path.includes("\\") || path.startsWith("/")) {
    throwInvalidPacketStoragePath();
  }

  for (const segment of path.split("/")) {
    if (segment === "" || segment === "." || segment === "..") {
      throwInvalidPacketStoragePath();
    }

    if (segment.includes("%")) {
      let decoded: string;
      try {
        decoded = decodeURIComponent(segment);
      } catch {
        throwInvalidPacketStoragePath();
      }

      if (decoded === "." || decoded === ".." || decoded.includes("/")) {
        throwInvalidPacketStoragePath();
      }
    }
  }
}

/**
 * Normalize a packet-relative path using the same URL pathname resolution as object-store clients.
 */
export function normalizePacketRelativePath(path: string): string {
  const url = new URL("https://trigger.invalid");
  url.pathname = `${PACKET_RELATIVE_PATH_BASE}/${path.replace(/^\/+/, "")}`;

  const prefix = `${PACKET_RELATIVE_PATH_BASE}/`;
  if (!url.pathname.startsWith(prefix)) {
    throwInvalidPacketStoragePath();
  }

  return url.pathname.slice(prefix.length);
}

/**
 * Ensure a full logical object-store key resolves under the packet prefix after URL normalization.
 */
export function assertPacketObjectStoreKeyUnderPrefix(key: string, packetPrefix: string): void {
  const normalizedKeyPath = normalizeObjectStoreLogicalKeyPathname(key);
  const normalizedPrefixPath = normalizeObjectStoreLogicalKeyPathname(packetPrefix);

  if (
    normalizedKeyPath !== normalizedPrefixPath &&
    !normalizedKeyPath.startsWith(`${normalizedPrefixPath}/`)
  ) {
    throwInvalidPacketStoragePath();
  }
}

/**
 * Validate a packet-relative path and return the canonical form used for object-store keys.
 */
export function resolveSafePacketRelativePath(path: string): string {
  assertRawPacketRelativePathSegments(path);
  const normalized = normalizePacketRelativePath(path);
  assertRawPacketRelativePathSegments(normalized);
  return normalized;
}

/**
 * Reject path traversal and other unsafe packet-relative storage paths before
 * building object-store keys or presigned URLs.
 */
export function assertSafePacketRelativePath(path: string): void {
  resolveSafePacketRelativePath(path);
}

function buildPacketObjectStoreKey(
  projectRef: string,
  envSlug: string,
  relativePath: string
): string {
  const safeRelativePath = resolveSafePacketRelativePath(relativePath);
  const prefix = `packets/${projectRef}/${envSlug}`;
  const key = `${prefix}/${safeRelativePath}`;
  assertPacketObjectStoreKeyUnderPrefix(key, prefix);
  return key;
}

/** JSON response for packet presign failures (400 client error vs 500 internal). */
export function jsonPacketPresignFailure(failure: PacketPresignFailure) {
  const status = failure.status ?? 500;
  if (status === 400) {
    return json({ error: failure.error }, { status: 400 });
  }

  return json({ error: `Failed to generate presigned URL: ${failure.error}` }, { status: 500 });
}

/**
 * Get object storage configuration for a given protocol.
 * Returns a config if baseUrl is set, even without explicit credentials —
 * in that case the AWS credential chain (ECS task role, EC2 IMDS, etc.) is used,
 * and OBJECT_STORE_BUCKET must also be set.
 */
function getObjectStoreConfig(protocol?: string): ObjectStoreClientConfig | undefined {
  if (protocol) {
    // Named provider (e.g., OBJECT_STORE_S3_*)
    const prefix = `OBJECT_STORE_${protocol.toUpperCase()}_`;
    const baseUrl = process.env[`${prefix}BASE_URL`];
    if (!baseUrl) return undefined;

    return {
      baseUrl,
      bucket: process.env[`${prefix}BUCKET`] || undefined,
      accessKeyId: process.env[`${prefix}ACCESS_KEY_ID`] || undefined,
      secretAccessKey: process.env[`${prefix}SECRET_ACCESS_KEY`] || undefined,
      region: process.env[`${prefix}REGION`] || undefined,
      service: process.env[`${prefix}SERVICE`] || undefined,
    };
  }

  // Default provider (backward compatible)
  if (!env.OBJECT_STORE_BASE_URL) {
    return undefined;
  }

  return {
    baseUrl: env.OBJECT_STORE_BASE_URL,
    bucket: env.OBJECT_STORE_BUCKET || undefined,
    accessKeyId: env.OBJECT_STORE_ACCESS_KEY_ID || undefined,
    secretAccessKey: env.OBJECT_STORE_SECRET_ACCESS_KEY || undefined,
    region: env.OBJECT_STORE_REGION || undefined,
    service: env.OBJECT_STORE_SERVICE || undefined,
  };
}

/**
 * Object storage client registry. Maps protocol name to ObjectStoreClient singleton.
 * ObjectStoreClient internally uses either aws4fetch (static credentials) or the
 * AWS SDK S3Client (IAM credential chain), selected at creation time.
 */
const objectStoreClients = singleton(
  "objectStoreClients",
  () => new Map<string, ObjectStoreClient>()
);

function getObjectStoreClient(protocol?: string): ObjectStoreClient | undefined {
  const config = getObjectStoreConfig(protocol);
  if (!config) return undefined;

  // Key includes baseUrl so that config changes (e.g. different containers in tests)
  // always produce a fresh client while production usage (stable env) is effectively
  // a per-protocol singleton.
  const cacheKey = `${protocol ?? "default"}:${config.baseUrl}`;
  if (objectStoreClients.has(cacheKey)) {
    return objectStoreClients.get(cacheKey);
  }

  const client = ObjectStoreClient.create(config);
  objectStoreClients.set(cacheKey, client);
  return client;
}

export function hasObjectStoreClient(): boolean {
  const defaultConfig = getObjectStoreConfig();
  const protocolConfig = env.OBJECT_STORE_DEFAULT_PROTOCOL
    ? getObjectStoreConfig(env.OBJECT_STORE_DEFAULT_PROTOCOL)
    : undefined;
  return !!(defaultConfig || protocolConfig);
}

export async function uploadPacketToObjectStore(
  filename: string,
  data: ReadableStream | string,
  contentType: string,
  environment: AuthenticatedEnvironment,
  storageProtocol?: string
): Promise<string> {
  const protocol = storageProtocol || env.OBJECT_STORE_DEFAULT_PROTOCOL;
  const client = getObjectStoreClient(protocol);

  if (!client) {
    throw new Error(`Object store is not configured for protocol: ${protocol || "default"}`);
  }

  const { path } = parseStorageUri(filename);
  const safePath = resolveSafePacketRelativePath(path);
  const key = buildPacketObjectStoreKey(
    environment.project.externalRef,
    environment.slug,
    safePath
  );

  logger.debug("Uploading to object store", { key, protocol: protocol || "default" });

  await client.putObject(key, data, contentType);

  // Return canonical storage URI (path only in the key; protocol prefix applied here)
  return formatStorageUri(safePath, protocol);
}

export async function downloadPacketFromObjectStore(
  packet: IOPacket,
  environment: AuthenticatedEnvironment
): Promise<IOPacket> {
  if (packet.dataType !== "application/store") {
    return packet;
  }

  // There shouldn't be an offloaded packet with undefined data…
  if (!packet.data) {
    logger.error("Object store packet has undefined data", { packet, environment });
    return {
      dataType: "application/json",
      data: undefined,
    };
  }

  const { protocol, path } = parseStorageUri(packet.data);
  const key = buildPacketObjectStoreKey(environment.project.externalRef, environment.slug, path);

  const client = getObjectStoreClient(protocol);

  if (!client) {
    throw new Error(`Object store is not configured for protocol: ${protocol || "default"}`);
  }

  logger.debug("Downloading from object store", { key, protocol: protocol || "default" });

  const data = await client.getObject(key);

  return { data, dataType: "application/json" };
}

export type GeneratePacketPresignOptions = {
  /**
   * When true (v1 packet PUT only), unprefixed keys use the legacy default object store only.
   * When false/omitted (v2 packet PUT), unprefixed keys also use OBJECT_STORE_DEFAULT_PROTOCOL.
   * Ignored for GET — reads never infer protocol from env for unprefixed keys.
   */
  forceNoPrefix?: boolean;
};

/**
 * Resolve object-store protocol for packet presigns.
 * GET: never apply OBJECT_STORE_DEFAULT_PROTOCOL to unprefixed keys.
 * PUT: optional forceNoPrefix for v1 legacy upload behavior.
 */
export function resolveStoreProtocolForPacketPresign(
  filename: string,
  method: "PUT" | "GET",
  forceNoPrefix?: boolean
): { path: string; storeProtocol: string | undefined } {
  const { protocol: explicitProtocol, path } = parseStorageUri(filename);

  if (method === "GET") {
    return { path, storeProtocol: explicitProtocol };
  }

  if (explicitProtocol !== undefined) {
    return { path, storeProtocol: explicitProtocol };
  }

  if (forceNoPrefix) {
    return { path, storeProtocol: undefined };
  }

  return { path, storeProtocol: env.OBJECT_STORE_DEFAULT_PROTOCOL };
}

export async function generatePresignedRequest(
  projectRef: string,
  envSlug: string,
  filename: string,
  method: "PUT" | "GET" = "PUT",
  options?: GeneratePacketPresignOptions
): Promise<
  | PacketPresignFailure
  | {
      success: true;
      request: Request;
      /** Canonical pointer for IOPacket.data (PUT only). */
      storagePath?: string;
    }
> {
  const { path, storeProtocol } = resolveStoreProtocolForPacketPresign(
    filename,
    method,
    options?.forceNoPrefix
  );

  let safePath: string;
  try {
    safePath = resolveSafePacketRelativePath(path);
  } catch (error) {
    if (error instanceof ServiceValidationError) {
      return {
        success: false,
        error: error.message,
        status: error.status ?? 400,
      };
    }

    throw error;
  }

  const config = getObjectStoreConfig(storeProtocol);
  if (!config?.baseUrl) {
    return {
      success: false,
      error: `Object store is not configured for protocol: ${storeProtocol || "default"}`,
    };
  }

  const client = getObjectStoreClient(storeProtocol);
  if (!client) {
    return {
      success: false,
      error: `Object store is not configured for protocol: ${storeProtocol || "default"}`,
    };
  }

  const key = buildPacketObjectStoreKey(projectRef, envSlug, safePath);

  try {
    const url = await client.presign(key, method, 300); // 5 minutes

    logger.debug("Generated presigned URL", {
      url,
      projectRef,
      envSlug,
      filename,
      protocol: storeProtocol || "default",
    });

    const storagePath = method === "PUT" ? formatStorageUri(safePath, storeProtocol) : undefined;

    return {
      success: true,
      request: new Request(url, { method }),
      storagePath,
    };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error.message : String(error),
    };
  }
}

export async function generatePresignedUrl(
  projectRef: string,
  envSlug: string,
  filename: string,
  method: "PUT" | "GET" = "PUT",
  options?: GeneratePacketPresignOptions
): Promise<PacketPresignFailure | { success: true; url: string; storagePath?: string }> {
  const signed = await generatePresignedRequest(projectRef, envSlug, filename, method, options);

  if (!signed.success) {
    return {
      success: false,
      error: signed.error,
      status: signed.status,
    };
  }

  return {
    success: true,
    url: signed.request.url,
    storagePath: signed.storagePath,
  };
}
