import db from "../db.server";
import { adminLog } from "./admin-logger.server";
import { getRuleByKey } from "./rules";

/**
 * URLMappingService (CDC LQ-01/02).
 *
 * Pre-computes a JSON mapping for the shop's CUSTOM rules (handles + paths) and
 * pushes it to the `cs_x9k2m.url_map` metafield. The Liquid theme block reads
 * this map for O(1) handle lookup and a single tight loop over paths, instead
 * of splitting CSVs at request time.
 *
 * The map intentionally does NOT cover template-based rules (collection.all,
 * cart, checkout, ...). Those are handled by Liquid template conditions which
 * are already cheap.
 */

interface ShopifyAdmin {
  graphql: (
    query: string,
    options?: { variables?: Record<string, unknown> },
  ) => Promise<Response>;
}

export interface UrlMapEntry {
  robots: string;
  canonical?: string;
}

export interface PathEntry extends UrlMapEntry {
  prefix: string;
}

export interface UrlMap {
  handles: Record<string, UrlMapEntry>;
  paths: PathEntry[];
  generatedAt: string;
}

interface SyncResult {
  success: boolean;
  error: string | null;
  size?: number;
}

const MAX_METAFIELD_JSON_SIZE = 30_000; // 30KB safety margin under Shopify's 32KB hard cap

const HANDLES_RULE = "noindex_handles";
const PATHS_RULE = "noindex_specific_url";

/**
 * Parse the CSV-ish customValue into a deduplicated, trimmed array.
 * Accepts comma-separated OR newline-separated entries.
 */
function parseCsv(raw: string | null | undefined): string[] {
  if (!raw) return [];
  const seen = new Set<string>();
  const out: string[] = [];
  for (const part of raw.split(/[,\n\r]+/)) {
    const trimmed = part.trim();
    if (!trimmed) continue;
    if (seen.has(trimmed)) continue;
    seen.add(trimmed);
    out.push(trimmed);
  }
  return out;
}

function normalizeHandle(value: string): string {
  // Strip leading/trailing slashes; the Liquid block compares against `handle`.
  return value.replace(/^\/+|\/+$/g, "");
}

function normalizePathPrefix(value: string): string {
  let v = value.trim();
  if (!v) return v;
  if (!v.startsWith("/")) v = `/${v}`;
  // Drop trailing slash so prefix matches both /foo and /foo/bar.
  if (v.length > 1 && v.endsWith("/")) v = v.slice(0, -1);
  return v;
}

export async function generate(shopId: string): Promise<UrlMap> {
  const handlesRule = getRuleByKey(HANDLES_RULE);
  const pathsRule = getRuleByKey(PATHS_RULE);

  const rows = await db.shopRule.findMany({
    where: {
      shopId,
      ruleKey: { in: [HANDLES_RULE, PATHS_RULE] },
    },
  });

  const handlesRow = rows.find((r) => r.ruleKey === HANDLES_RULE);
  const pathsRow = rows.find((r) => r.ruleKey === PATHS_RULE);

  const handles: Record<string, UrlMapEntry> = {};
  if (handlesRule && handlesRow?.enabled && handlesRow.customValue) {
    for (const raw of parseCsv(handlesRow.customValue)) {
      const handle = normalizeHandle(raw);
      if (!handle) continue;
      handles[handle] = { robots: handlesRule.robotsValue };
    }
  }

  const paths: PathEntry[] = [];
  if (pathsRule && pathsRow?.enabled && pathsRow.customValue) {
    for (const raw of parseCsv(pathsRow.customValue)) {
      const prefix = normalizePathPrefix(raw);
      if (!prefix) continue;
      paths.push({ prefix, robots: pathsRule.robotsValue });
    }
  }

  return {
    handles,
    paths,
    generatedAt: new Date().toISOString(),
  };
}

async function getAppInstallationId(admin: ShopifyAdmin): Promise<string> {
  const response = await admin.graphql(
    `#graphql
    query {
      currentAppInstallation {
        id
      }
    }`,
  );
  const data = (await response.json()) as {
    data: { currentAppInstallation: { id: string } };
  };
  return data.data.currentAppInstallation.id;
}

export async function sync(
  shopId: string,
  admin: ShopifyAdmin,
): Promise<SyncResult> {
  const map = await generate(shopId);
  const json = JSON.stringify(map);

  if (json.length > MAX_METAFIELD_JSON_SIZE) {
    await adminLog(
      "error",
      `url_map metafield payload too large: ${json.length} bytes`,
      { shopId },
    );
    return {
      success: false,
      error: `Le mapping URL depasse ${MAX_METAFIELD_JSON_SIZE / 1000}KB. Reduisez les valeurs personnalisees.`,
      size: json.length,
    };
  }

  // Best-effort cache write (parallel branch may add app/lib/cache.server.ts).
  // We use a runtime-resolved specifier so TypeScript does not error when the
  // module is absent. If/when the cache helper lands, no code change is needed
  // here — this block will start populating it automatically.
  try {
    const cacheModuleName = "./cache.server";
    const cacheMod = (await import(/* @vite-ignore */ cacheModuleName).catch(
      () => null,
    )) as
      | { cacheSet?: (key: string, value: unknown, ttl: number) => unknown }
      | null;
    if (cacheMod?.cacheSet) {
      await Promise.resolve(cacheMod.cacheSet(`url_map:${shopId}`, map, 300));
    }
  } catch {
    // ignore — cache is optional
  }

  try {
    const ownerId = await getAppInstallationId(admin);
    const response = await admin.graphql(
      `#graphql
      mutation SetUrlMap($metafields: [MetafieldsSetInput!]!) {
        metafieldsSet(metafields: $metafields) {
          metafields { id namespace key }
          userErrors { field message }
        }
      }`,
      {
        variables: {
          metafields: [
            {
              namespace: "cs_x9k2m",
              key: "url_map",
              type: "json",
              value: json,
              ownerId,
            },
          ],
        },
      },
    );

    const data = (await response.json()) as {
      data?: { metafieldsSet?: { userErrors?: { message: string }[] } };
    };

    const userErrors = data.data?.metafieldsSet?.userErrors ?? [];
    if (userErrors.length > 0) {
      await adminLog(
        "error",
        `url_map sync userError: ${userErrors[0].message}`,
        { shopId },
      );
      return { success: false, error: userErrors[0].message, size: json.length };
    }

    return { success: true, error: null, size: json.length };
  } catch (error) {
    const msg = error instanceof Error ? error.message : String(error);
    await adminLog("error", `url_map sync network error: ${msg}`, { shopId });
    return { success: false, error: msg, size: json.length };
  }
}

export const urlMappingService = { generate, sync };
