/**
 * permissions.js - Shared Permissions Parsing Library
 *
 * Provides utilities for parsing permissions from CSV data and generating
 * permission key constants for Go and TypeScript.
 */

const logger = require("./logger");
const { parseCSV, loadCSV, toPascalCase, isValidUUID } = require("./csv");

// CSV column indices (0-based)
const COL_KEYCHAIN_ID = 0;
const COL_FUNCTION = 2;
const COL_FEATURE = 3;
const COL_KEY_ID = 29;

// Default number of header rows to skip in permissions CSV
const DEFAULT_SKIP_ROWS = 2;

/**
 * Generate constant name from category and function
 * @param {string} category - Category/Theme column value
 * @param {string} func - Function column value
 * @returns {string} PascalCase constant name
 */
function generateConstantName(category, func) {
  const categoryPascal = toPascalCase(category);
  const funcPascal = toPascalCase(func);
  return `${categoryPascal}${funcPascal}`;
}

/**
 * Parse permissions from CSV content
 * @param {string} csvContent - Raw CSV content
 * @param {Object} [options] - Parse options
 * @param {number} [options.skipRows=2] - Number of header rows to skip
 * @returns {Array<{name: string, uuid: string, feature: string}>} Parsed permissions
 */
function parsePermissions(csvContent, options = {}) {
  const { skipRows = DEFAULT_SKIP_ROWS } = options;
  const rows = parseCSV(csvContent, { skipRows });
  const permissions = [];
  const seenNames = new Set();

  for (let i = 0; i < rows.length; i++) {
    const fields = rows[i];
    const rowNum = i + skipRows + 1; // 1-based row number for logging

    const category = fields[COL_KEYCHAIN_ID] || "";
    const func = fields[COL_FUNCTION] || "";
    const feature = fields[COL_FEATURE] || "";
    const keyId = fields[COL_KEY_ID] || "";

    // Skip rows without valid data
    if (!category || !func || !keyId) {
      continue;
    }

    // Validate UUID
    if (!isValidUUID(keyId)) {
      logger.warn(`Skipping row ${rowNum}: Invalid UUID format "${keyId}"`);
      continue;
    }

    const constantName = generateConstantName(category, func);

    // Handle duplicate names
    if (seenNames.has(constantName)) {
      logger.warn(
        `Skipping duplicate constant name: ${constantName} (row ${rowNum})`,
      );
      continue;
    }
    seenNames.add(constantName);

    permissions.push({
      name: constantName,
      uuid: keyId,
      feature: feature,
      category: category,
      subcategory: fields[1] || "",
      function: func,
      description: feature,
    });
  }

  // Duplicate-UUID guard (shrink-only). A key UUID is a permission's
  // authorization identity: two distinct constants sharing one UUID means a
  // grant of either silently grants both. Fail generation when a UUID maps to
  // more than one constant name, unless the UUID is in the frozen allowlist
  // below. The allowlist is SHRINK-ONLY — remove an entry once the source
  // sheet assigns the colliding functions distinct UUIDs; never add a new one
  // (fix new collisions in the sheet). Tracked in meshery/schemas#930.
  const KNOWN_DUPLICATE_UUIDS = new Set([]);
  const uuidToNames = new Map();
  for (const p of permissions) {
    const u = p.uuid.toLowerCase();
    if (!uuidToNames.has(u)) uuidToNames.set(u, new Set());
    uuidToNames.get(u).add(p.name);
  }
  const unexpectedDupes = [];
  for (const [uuid, names] of uuidToNames) {
    if (names.size > 1 && !KNOWN_DUPLICATE_UUIDS.has(uuid)) {
      unexpectedDupes.push(`  ${uuid} -> ${[...names].join(", ")}`);
    }
  }
  if (unexpectedDupes.length > 0) {
    throw new Error(
      "Duplicate permission UUID(s): one UUID maps to multiple permission " +
        "constants. Each permission needs a unique UUID — assign distinct " +
        "UUIDs in the source sheet (meshery/schemas#930):\n" +
        unexpectedDupes.join("\n"),
    );
  }
  const staleAllowlist = [...KNOWN_DUPLICATE_UUIDS].filter(
    (u) => (uuidToNames.get(u)?.size ?? 0) <= 1,
  );
  if (staleAllowlist.length > 0) {
    throw new Error(
      "KNOWN_DUPLICATE_UUIDS is shrink-only: these entries no longer collide " +
        "and must be removed from the allowlist:\n  " +
        staleAllowlist.join("\n  "),
    );
  }

  return permissions;
}

/**
 * Load and parse permissions from a source (file path, URL, or Google Sheets)
 * @param {string} source - Local file path, URL, or Google Sheets URL
 * @param {Object} [options] - Load options
 * @param {string} [options.gid] - Optional Google Sheets tab GID
 * @param {number} [options.skipRows=2] - Number of header rows to skip
 * @returns {Promise<Array<{name: string, uuid: string, feature: string}>>} Parsed permissions
 */
async function loadPermissions(source, options = {}) {
  const csvContent = await loadCSV(source, options);
  const lines = csvContent.split("\n").length;
  logger.info(`Loaded CSV content (${lines} lines)`);
  return parsePermissions(csvContent, options);
}

/**
 * Generate Go file content for permissions
 * @param {Array<{name: string, uuid: string, feature: string}>} permissions - Parsed permissions
 * @returns {string} Go file content
 */
function generateGoFile(permissions, indexId) {
  const lines = [
    "// Package permissions contains auto-generated permission key constants.",
    "// This file is generated from permissions.csv - DO NOT EDIT MANUALLY.",
    "// To regenerate, run: node build/generate-permission-golang.js",
    "package permissions",
    'import "github.com/gofrs/uuid"',
    "",
    `// Index ID used to generate this file\nconst IndexID = "${indexId}"`,
    "",
    "",
    "// PermissionKey represents a permission key identifier.",
    "type PermissionKey uuid.UUID",
    "",
    "// PermissionKeyFromUUID converts a uuid.UUID to PermissionKey.",
    "func PermissionKeyFromUUID(id uuid.UUID) PermissionKey {",
    "\treturn PermissionKey(id)",
    "}",
    "",
    "// UUID returns the underlying uuid.UUID value.",
    "func (p PermissionKey) UUID() uuid.UUID {",
    "\treturn uuid.UUID(p)",
    "}",
    "",
    "// String returns the string representation of the permission key.",
    "func (p PermissionKey) String() string {",
    "\treturn uuid.UUID(p).String()",
    "}",
    "",
    "var (",
  ];

  for (const perm of permissions) {
    lines.push(
      `\t// ${perm.name} - ${perm.feature || "No description available"}`,
    );
    lines.push(
      `\t${perm.name} = PermissionKey(uuid.Must(uuid.FromString("${perm.uuid}")))`,
    );
    lines.push("");
  }

  lines.push(")");
  lines.push("");

  return lines.join("\n");
}

/**
 * Escape special characters for JSDoc comments
 * @param {string} str - String to escape
 * @returns {string} Escaped string
 */
function escapeJSDocComment(str) {
  return str.replace(/\*\//g, "*\\/").replace(/\n/g, " ");
}

/**
 * Generate TypeScript file content for permissions
 * @param {Array<{name: string, uuid: string, feature: string}>} permissions - Parsed permissions
 * @returns {string} TypeScript file content
 */
function generateTypeScriptFile(permissions, indexId) {
  const lines = [
    "/**",
    " * Permission key constants generated from permissions.csv",
    " * This file is auto-generated - DO NOT EDIT MANUALLY.",
    " * To regenerate, run: node build/generate-permissions-ts.js",
    " */",
    "",
    "/**",
    " * A branded type representing a permission key UUID.",
    " * This provides type safety while remaining compatible with string operations.",
    " */",
    "export type PermissionKey = string & { readonly __brand: 'PermissionKey' };",
    "",
    "/**",
    " * Interface representing a Key conforming to the Key schema.",
    " */",
    "export interface Key {",
    "  readonly id: PermissionKey;",
    "  readonly category: string;",
    "  readonly subcategory: string;",
    "  readonly function: string;",
    "  readonly description: string;",
    "}",
    "",
    "/**",
    " * Permissions Index ID used for this generated file.",
    " */",
    `export const PERMISSIONS_INDEX_ID = "${indexId}" as const;`,
    "",
    "/**",
    " * Creates a PermissionKey from a UUID string.",
    " * @param uuid - The UUID string to convert",
    " * @returns The branded PermissionKey",
    " */",
    "export function createPermissionKey(uuid: string): PermissionKey {",
    "  return uuid as PermissionKey;",
    "}",
    "",
    "/**",
    " * Keys conforming to the Key schema.",
    " */",
    "export const Keys = {",
  ];

  for (let i = 0; i < permissions.length; i++) {
    const perm = permissions[i];
    const isLast = i === permissions.length - 1;
    const comma = isLast ? "" : ",";

    lines.push(`  /**`);
    lines.push(
      `   * ${escapeJSDocComment(perm.description || perm.feature || "No description available")}`,
    );
    lines.push(`   */`);
    lines.push(`  ${perm.name}: {`);
    lines.push(`    id: "${perm.uuid}" as PermissionKey,`);
    lines.push(`    category: "${perm.category ? perm.category.replace(/"/g, '\\"') : ""}",`);
    lines.push(`    subcategory: "${perm.subcategory ? perm.subcategory.replace(/"/g, '\\"') : ""}",`);
    lines.push(`    function: "${perm.function ? perm.function.replace(/"/g, '\\"') : ""}",`);
    lines.push(`    description: "${perm.description || perm.feature ? (perm.description || perm.feature).replace(/"/g, '\\"') : ""}"`);
    lines.push(`  }${comma}`);
    if (!isLast) {
      lines.push("");
    }
  }

  lines.push("} as const;");
  lines.push("");
  lines.push("/**");
  lines.push(" * Permission key constants.");
  lines.push(" * Each key represents a unique permission identified by its UUID.");
  lines.push(" */");
  lines.push("export const PermissionKeys = {");

  for (let i = 0; i < permissions.length; i++) {
    const perm = permissions[i];
    const isLast = i === permissions.length - 1;
    const comma = isLast ? "" : ",";

    lines.push(`  /**`);
    lines.push(
      `   * ${escapeJSDocComment(perm.description || perm.feature || "No description available")}`,
    );
    lines.push(`   */`);
    lines.push(`  ${perm.name}: "${perm.uuid}" as PermissionKey${comma}`);
    if (!isLast) {
      lines.push("");
    }
  }

  lines.push("} as const;");
  lines.push("");
  lines.push("/**");
  lines.push(" * Type representing all valid permission key names.");
  lines.push(" */");
  lines.push("export type PermissionKeyName = keyof typeof PermissionKeys;");
  lines.push("");
  lines.push("/**");
  lines.push(" * Type representing all valid permission key values.");
  lines.push(" */");
  lines.push(
    "export type PermissionKeyValue = typeof PermissionKeys[PermissionKeyName];",
  );
  lines.push("");
  lines.push("/**");
  lines.push(" * Array of all permission keys for iteration.");
  lines.push(" */");
  lines.push("export const AllPermissionKeys = Object.values(PermissionKeys);");
  lines.push("");
  lines.push("/**");
  lines.push(" * Array of all permission key names for iteration.");
  lines.push(" */");
  lines.push(
    "export const AllPermissionKeyNames = Object.keys(PermissionKeys) as PermissionKeyName[];",
  );
  lines.push("");

  return lines.join("\n");
}

const path = require("path");
const fs = require("fs");
const paths = require("./paths");

const crypto = require("crypto");

function computeIndexId(permissions) {
  // Stable canonical string using sorted name+uuid, ignoring feature
  const entries = permissions
    .map((p) => ({ name: p.name, uuid: p.uuid }))
    .sort((a, b) =>
      a.name === b.name
        ? a.uuid.localeCompare(b.uuid)
        : a.name.localeCompare(b.name),
    )
    .map((e) => `${e.name}:${e.uuid}`)
    .join("\n");
  return crypto.createHash("sha256").update(entries).digest("hex");
}

function buildIndex(permissions) {
  const id = computeIndexId(permissions);
  return {
    id,
    // createdAt: new Date().toISOString(),
    count: permissions.length,
    items: permissions.map((p) => ({
      name: p.name,
      uuid: p.uuid,
      feature: p.feature,
      category: p.category,
      subcategory: p.subcategory,
      function: p.function,
      description: p.description,
    })),
  };
}

function saveIndex(index) {
  const dir = paths.fromRoot("permissions_history");
  paths.ensureDir(dir);
  const file = path.join(dir, `${index.id}-permissions_index.json`);
  fs.writeFileSync(file, JSON.stringify(index, null, 2), "utf-8");
  return file;
}

function loadIndex(filePath) {
  const data = fs.readFileSync(filePath, "utf-8");
  return JSON.parse(data);
}

function diffIndexes(oldIndex, newIndex) {
  const oldById = new Map(oldIndex.items.map((i) => [i.uuid, i]));
  const newById = new Map(newIndex.items.map((i) => [i.uuid, i]));

  const added = [];
  const removed = [];
  const updated = [];

  // Added and updated (by id)
  for (const [id, item] of newById.entries()) {
    if (!oldById.has(id)) {
      added.push({ id, name: item.name });
    } else {
      const prev = oldById.get(id);
      if (prev.name !== item.name) {
        updated.push({ id, old_name: prev.name, new_name: item.name });
      }
    }
  }

  // Removed (by id)
  for (const [id, item] of oldById.entries()) {
    if (!newById.has(id)) {
      removed.push({ id, name: item.name });
    }
  }

  return { added, removed, updated };
}

module.exports = {
  COL_CATEGORY: COL_KEYCHAIN_ID,
  COL_FUNCTION,
  COL_FEATURE,
  COL_KEY_ID,
  DEFAULT_SKIP_ROWS,
  generateConstantName,
  parsePermissions,
  loadPermissions,
  generateGoFile,
  generateTypeScriptFile,
  escapeJSDocComment,
  computeIndexId,
  buildIndex,
  saveIndex,
  loadIndex,
  diffIndexes,
};
