#!/usr/bin/env node
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { dirname, relative, resolve } from "node:path";
import { stdin as input, stdout as output } from "node:process";
import { createInterface } from "node:readline/promises";

const cwd = process.cwd();
const args = parseArgs(process.argv.slice(2));
const defaultBackendUrl = "https://service.orderly.shop";

if (args.help || args.h) {
  printHelp();
  process.exit(0);
}

const target = resolve(cwd, args.path ?? "navigation.ts");
const productDumpTarget = resolve(cwd, args["product-dump-path"] ?? defaultProductDumpPath(target));

if (!args.suggest && existsSync(target) && !args.force) {
  throw new Error(`${relative(cwd, target)} already exists. Re-run with --force to overwrite it.`);
}
if (args.suggest && existsSync(productDumpTarget) && !args.force) {
  throw new Error(`${relative(cwd, productDumpTarget)} already exists. Re-run with --force to overwrite it.`);
}

const generated = args.suggest
  ? await suggestedProductDump()
  : { navigation: defaultNavigationSource() };

if (generated.navigation) {
  mkdirSync(dirname(target), { recursive: true });
  writeFileSync(target, generated.navigation, "utf8");
  console.log(`Created ${relative(cwd, target)}.`);
}
if (generated.productDump) {
  mkdirSync(dirname(productDumpTarget), { recursive: true });
  writeFileSync(productDumpTarget, generated.productDump, "utf8");
  console.log(`Created ${relative(cwd, productDumpTarget)}.`);
}
if (generated.nextStep) {
  console.log(generated.nextStep);
}

async function suggestedProductDump() {
  const accountId = await promptText({
    label: "Account ID for the new shop",
    value: args["account-id"],
    required: true
  });
  const baseUrl = await promptText({
    label: "Orderly backend URL",
    value: args["base-url"] ?? defaultBackendUrl,
    required: true
  });
  const maxResults = positiveInteger(args["max-results"] ?? args.limit ?? 1000, "max results");
  const suggestionCount = positiveInteger(args["suggestion-count"] ?? 12, "suggestion count");
  const result = await fetchProductSample({ accountId, baseUrl, maxResults, suggestionCount });
  return {
    productDump: productDumpSource({ accountId, baseUrl, maxResults, result }),
    nextStep: `Use ${relative(cwd, productDumpTarget)} to create ${relative(cwd, target)} manually. Review querySyntax and navigationSuggestions; each suggestion includes a backend match count for the generated query input. Define an explicit unique slug for every category. Configure the shop scope with a SearchQuery and pass it to configureShop({ defaultShop: { defaultQuery } }). Keep navigation to at most two category levels.`
  };
}

async function fetchProductSample({ accountId, baseUrl, maxResults, suggestionCount }) {
  const [
    { create, clone },
    { anyUnpack },
    {
      AccountIdSchema,
      SearchObjectSchema,
      SearchQuerySchema,
      TagSchema,
      UUIDSchema
    },
    { createOrderlyNodeClient }
  ] = await Promise.all([
    import("@bufbuild/protobuf"),
    import("@bufbuild/protobuf/wkt"),
    import("@orderlyshop/core-client"),
    import("@orderlyshop/core-client/node")
  ]);
  const client = createOrderlyNodeClient({ baseUrl, protocol: "grpc" });
  const sourceAccountId = create(AccountIdSchema, {
    Id: create(UUIDSchema, { Value: accountId })
  });
  const products = [];
  let scanned = 0;
  let query = create(SearchQuerySchema, {
    Query: "",
    SourceAccountId: sourceAccountId,
    OrderBy: ["CreatedTime desc"]
  });
  while (products.length < maxResults) {
    const result = await client.services.searchService.search(query);
    for (const product of unpackProducts(result, { anyUnpack, SearchObjectSchema })) {
      scanned += 1;
      if (shouldSampleProduct(scanned)) {
        products.push(productSummary(product));
      }
      if (products.length >= maxResults) {
        break;
      }
    }
    if (!result.Continuation?.Token) {
      break;
    }
    query = clone(SearchQuerySchema, query);
    query.Continuation = result.Continuation;
  }
  const navigationSuggestions = await validatedNavigationSuggestions({
    client,
    create,
    products,
    SearchQuerySchema,
    sourceAccountId,
    suggestionCount,
    TagSchema
  });
  return { products, scanned, navigationSuggestions };
}

function unpackProducts(result, { anyUnpack, SearchObjectSchema }) {
  return result.data
    .map((item) => anyUnpack(item, SearchObjectSchema))
    .filter(Boolean);
}

async function promptText({ label, value, required }) {
  if (value) {
    return value;
  }
  if (!input.isTTY) {
    throw new Error(`${label} is required. Pass it as an option or run in an interactive terminal.`);
  }
  const rl = createInterface({ input, output });
  try {
    const answer = (await rl.question(`${label}: `)).trim();
    if (required && !answer) {
      throw new Error(`${label} is required.`);
    }
    return answer;
  } finally {
    rl.close();
  }
}

function parseArgs(values) {
  const parsed = {};
  for (let index = 0; index < values.length; index += 1) {
    const value = values[index];
    if (!value.startsWith("--")) {
      continue;
    }
    const [name, inlineValue] = value.slice(2).split("=", 2);
    if (inlineValue !== undefined) {
      parsed[name] = inlineValue;
    } else if (values[index + 1] && !values[index + 1].startsWith("--")) {
      parsed[name] = values[index + 1];
      index += 1;
    } else {
      parsed[name] = true;
    }
  }
  return parsed;
}

function printHelp() {
  console.log(`Usage: orderly-init-navigation [options]

Create a default strongly typed navigation.ts file, or use --suggest to dump product data for agent-assisted navigation design.

Options:
  --path <file>          Output path. Defaults to navigation.ts.
  --product-dump-path <file>
                         Output path for sampled product information with --suggest.
                         Defaults to navigation-products.json next to navigation.ts.
  --force                Overwrite an existing output file.
  --suggest              Dump sampled product information through SearchService.
  --account-id <id>      Orderly account id to sample with --suggest.
  --base-url <url>       Orderly backend URL. Defaults to ${defaultBackendUrl}.
  --max-results <number> Max products to write to the dump. Defaults to 1000.
  --limit <number>       Alias for --max-results.
  --suggestion-count <n> Number of validated query suggestions to include. Defaults to 12.
  --help, -h             Show this help.

Agent workflow:
  1. Ask the user for the shop account id.
  2. Run: npx orderly-init-navigation --suggest --account-id <account-id>
  3. Read querySyntax and navigationSuggestions in the generated product dump before writing queries.
  4. Export navigationDefinitions: NavigationDefinition[] with explicit unique slug values on every category.
  5. Configure tenant scoping in the shop setup with a SearchQuery passed to configureShop({ defaultShop: { defaultQuery } }).
  6. Keep navigation to at most two category levels and avoid categories with 0 or very few matching products.
`);
}

function defaultNavigationSource() {
  return navigationSource(defaultNavigationDefinitions(), {}, target.endsWith(".ts") || target.endsWith(".tsx"));
}

function navigationSource(definitions, metadata = {}, typed = false) {
  const metadataLines = [];
  if (metadata.generatedFromAccountId) {
    metadataLines.push(`// Generated from AccountId: ${metadata.generatedFromAccountId}`);
  }
  if (metadata.generatedFromProductCount !== undefined) {
    metadataLines.push(`// Products sampled: ${metadata.generatedFromProductCount}`);
  }
  return `${metadataLines.length ? `${metadataLines.join("\n")}\n` : ""}${typed ? "import type { NavigationDefinition } from \"@orderlyshop/web-components\";\n\n" : ""}export const navigationDefinitions${typed ? ": NavigationDefinition[]" : ""} = ${formatNavigationDefinitions(definitions)};
`;
}

function formatNavigationDefinitions(definitions) {
  return `[\n${definitions.map((definition) => `${formatNavigationDefinition(definition, 2)}`).join(",\n")}\n]`;
}

function formatNavigationDefinition(definition, indent) {
  const pad = " ".repeat(indent);
  const childPad = " ".repeat(indent + 2);
  const lines = [
    `${pad}{`,
    `${childPad}label: ${JSON.stringify(definition.label)},`,
    `${childPad}slug: ${JSON.stringify(definition.slug)},`
  ];
  if (definition.heroImage) {
    lines.push(`${childPad}heroImage: ${JSON.stringify(definition.heroImage)},`);
  }
  if (definition.query) {
    lines.push(`${childPad}query: { query: ${JSON.stringify(definition.query)} }`);
  }
  if (definition.children?.length) {
    if (definition.query) {
      lines[lines.length - 1] += ",";
    }
    lines.push(`${childPad}children: [`);
    lines.push(definition.children.map((child) => formatNavigationDefinition(child, indent + 4)).join(",\n"));
    lines.push(`${childPad}]`);
  }
  lines.push(`${pad}}`);
  return lines.join("\n");
}

function productDumpSource({ accountId, baseUrl, maxResults, result }) {
  return `${JSON.stringify({
    generatedAt: new Date().toISOString(),
    accountId,
    baseUrl,
    maxResults,
    querySyntax: querySyntaxSummary(),
    sourceOrder: ["CreatedTime desc"],
    sampling: {
      strategy: "source position 1-200: every 2nd product; 201-500: every 3rd; 501-1000: every 5th; 1001-2000: every 8th; 2001+: every 13th",
      sourceProductsScanned: result.scanned,
      productsWritten: result.products.length
    },
    navigationSuggestions: result.navigationSuggestions ?? [],
    products: result.products
  }, null, 2)}\n`;
}

function querySyntaxSummary() {
  return {
    query: "SearchQuery.Query / keywords uses the backend text query syntax. Whitespace is narrowing, not OR.",
    orOperator: "Use | for broad alternatives, for example \"lego | matchbox | playmobil\".",
    tags: "Multiple SearchQuery.Tags are narrowing AND-style filters. Do not use tags as comma-separated alternatives for broad categories.",
    broadCategoryRecommendation: "For broad category rails, prefer a tested OR query in Query and keep Tags for confirmed shared tags only."
  };
}

async function validatedNavigationSuggestions({
  client,
  create,
  products,
  SearchQuerySchema,
  sourceAccountId,
  suggestionCount,
  TagSchema
}) {
  const candidates = navigationSuggestionCandidates(products, suggestionCount * 3);
  const validated = [];
  for (const candidate of candidates) {
    try {
      const result = await client.services.searchService.search(create(SearchQuerySchema, {
        Query: candidate.queryInput.query,
        SourceAccountId: sourceAccountId,
        Tags: (candidate.queryInput.tags ?? []).map((tag) => create(TagSchema, { Value: tag })),
        OrderBy: ["CreatedTime desc"]
      }));
      const backendCount = result.EstimatedTotalCount ?? result.Count;
      const matchedProducts = backendCount > 0 ? backendCount : result.data.length;
      if (matchedProducts > 0) {
        validated.push({
          ...candidate,
          matchedProducts
        });
      }
    } catch (error) {
      validated.push({
        ...candidate,
        error: error instanceof Error ? error.message : String(error)
      });
    }
  }
  return validated
    .sort((left, right) => (right.matchedProducts ?? 0) - (left.matchedProducts ?? 0) || right.sampleProductCount - left.sampleProductCount)
    .slice(0, suggestionCount);
}

function navigationSuggestionCandidates(products, limit) {
  const candidates = [];
  const tags = topFrequencies(products.flatMap((product) => product.tags), limit);
  const brands = topFrequencies(products.map((product) => product.brand), limit);
  const titleTokens = topFrequencies(products.flatMap((product) => titleTokensForSuggestions(product.title)), limit);
  for (const item of tags) {
    candidates.push({
      label: item.value,
      source: "tag",
      sampleProductCount: item.count,
      queryInput: { tags: [item.value] }
    });
  }
  for (const item of brands) {
    candidates.push({
      label: item.value,
      source: "brand",
      sampleProductCount: item.count,
      queryInput: { query: item.value }
    });
  }
  for (const item of titleTokens) {
    candidates.push({
      label: item.value,
      source: "title-token",
      sampleProductCount: item.count,
      queryInput: { query: item.value }
    });
  }
  const broadBrandQuery = orQuery(brands.slice(0, 4).map((item) => item.value));
  if (broadBrandQuery) {
    candidates.unshift({
      label: "Broad brand group",
      source: "or-query",
      sampleProductCount: brands.slice(0, 4).reduce((sum, item) => sum + item.count, 0),
      queryInput: { query: broadBrandQuery }
    });
  }
  return uniqueSuggestionCandidates(candidates).slice(0, limit);
}

function topFrequencies(values, limit) {
  const frequencies = new Map();
  for (const raw of values) {
    const value = normalizedSuggestionValue(raw);
    if (!value) {
      continue;
    }
    frequencies.set(value, (frequencies.get(value) ?? 0) + 1);
  }
  return [...frequencies.entries()]
    .map(([value, count]) => ({ value, count }))
    .sort((left, right) => right.count - left.count || left.value.localeCompare(right.value, "da"))
    .slice(0, limit);
}

function titleTokensForSuggestions(title) {
  return String(title ?? "")
    .split(/[^\p{L}\p{N}]+/u)
    .map((token) => token.trim().toLowerCase())
    .filter((token) => token.length >= 3 && !suggestionStopWords().has(token));
}

function normalizedSuggestionValue(value) {
  const normalized = String(value ?? "").replace(/\s+/g, " ").trim();
  if (!normalized || suggestionStopWords().has(normalized.toLowerCase())) {
    return "";
  }
  return normalized;
}

function suggestionStopWords() {
  return new Set([
    "and",
    "den",
    "der",
    "det",
    "for",
    "fra",
    "med",
    "og",
    "the",
    "til",
    "uden"
  ]);
}

function orQuery(values) {
  const unique = uniqueStrings(values.map(normalizedSuggestionValue).filter(Boolean));
  return unique.length > 1 ? unique.join(" | ") : "";
}

function uniqueSuggestionCandidates(candidates) {
  const seen = new Set();
  const result = [];
  for (const candidate of candidates) {
    const key = JSON.stringify(candidate.queryInput);
    if (seen.has(key)) {
      continue;
    }
    seen.add(key);
    result.push(candidate);
  }
  return result;
}

function productSummary(product) {
  return {
    title: product.Title ?? "",
    brand: product.Brand ?? "",
    tags: uniqueStrings((product.Tags ?? []).map((tag) => tag.Value).filter(Boolean))
  };
}

function shouldSampleProduct(sourcePosition) {
  return (sourcePosition - 1) % sampleInterval(sourcePosition) === 0;
}

function sampleInterval(sourcePosition) {
  if (sourcePosition <= 200) {
    return 2;
  }
  if (sourcePosition <= 500) {
    return 3;
  }
  if (sourcePosition <= 1000) {
    return 5;
  }
  if (sourcePosition <= 2000) {
    return 8;
  }
  return 13;
}

function uniqueStrings(values) {
  return [...new Set(values)];
}

function positiveInteger(value, label) {
  const parsed = Number(value);
  if (!Number.isInteger(parsed) || parsed <= 0) {
    throw new Error(`${label} must be a positive integer.`);
  }
  return parsed;
}

function defaultProductDumpPath(navigationPath) {
  return resolve(dirname(navigationPath), "navigation-products.json");
}

function defaultNavigationDefinitions() {
  return [
  {
    label: "Clothing",
    slug: "clothing",
    heroImage: "https://images.unsplash.com/photo-1496747611176-843222e1e57c?auto=format&fit=crop&w=1800&q=80",
    query: "clothing | fashion",
    children: [
      { label: "Women", slug: "women", query: "women | dresses | jackets" },
      { label: "Men", slug: "men", query: "men | shirts | jackets" },
      { label: "Kids", slug: "kids", query: "kids | children" },
      { label: "Accessories", slug: "accessories", query: "accessories | bags | belts" }
    ]
  },
  {
    label: "Shoes",
    slug: "shoes",
    heroImage: "https://images.unsplash.com/photo-1542291026-7eec264c27ff?auto=format&fit=crop&w=1800&q=80",
    query: "shoes | sneakers",
    children: [
      { label: "Women shoes", slug: "women-shoes", query: "women shoes | sneakers | boots" },
      { label: "Men shoes", slug: "men-shoes", query: "men shoes | sneakers | boots" },
      { label: "Sport shoes", slug: "sport-shoes", query: "sport shoes | running" }
    ]
  },
  {
    label: "Home",
    slug: "home",
    heroImage: "https://images.unsplash.com/photo-1618221195710-dd6b41faaea6?auto=format&fit=crop&w=1800&q=80",
    query: "home | interior | design",
    children: [
      { label: "Furniture", slug: "furniture", query: "furniture | sofa | chair | table" },
      { label: "Lighting", slug: "lighting", query: "lamp | lighting" },
      { label: "Kitchen", slug: "kitchen", query: "kitchen | tableware | equipment" }
    ]
  }
];
}
