// Apply managed PR labels (size/risk/path/module/contributor tiers).
// Extracted from pr-labeler workflow inline github-script for maintainability.

module.exports = async ({ github, context, core }) => {
const pr = context.payload.pull_request;
const owner = context.repo.owner;
const repo = context.repo.repo;
const action = context.payload.action;
const changedLabel = context.payload.label?.name;

const sizeLabels = ["size: XS", "size: S", "size: M", "size: L", "size: XL"];
const computedRiskLabels = ["risk: low", "risk: medium", "risk: high"];
const manualRiskOverrideLabel = "risk: manual";
const managedEnforcedLabels = new Set([
  ...sizeLabels,
  manualRiskOverrideLabel,
  ...computedRiskLabels,
]);
if ((action === "labeled" || action === "unlabeled") && !managedEnforcedLabels.has(changedLabel)) {
  core.info(`skip non-size/risk label event: ${changedLabel || "unknown"}`);
  return;
}

async function loadContributorTierPolicy() {
  const policyPath = process.env.LABEL_POLICY_PATH || ".github/label-policy.json";
  const fallback = {
    contributorTierColor: "2ED9FF",
    contributorTierRules: [
      { label: "distinguished contributor", minMergedPRs: 50 },
      { label: "principal contributor", minMergedPRs: 20 },
      { label: "experienced contributor", minMergedPRs: 10 },
      { label: "trusted contributor", minMergedPRs: 5 },
    ],
  };
  try {
    const { data } = await github.rest.repos.getContent({
      owner,
      repo,
      path: policyPath,
      ref: context.payload.repository?.default_branch || "main",
    });
    const json = JSON.parse(Buffer.from(data.content, "base64").toString("utf8"));
    const contributorTierRules = (json.contributor_tiers || []).map((entry) => ({
      label: String(entry.label || "").trim(),
      minMergedPRs: Number(entry.min_merged_prs || 0),
    }));
    const contributorTierColor = String(json.contributor_tier_color || "").toUpperCase();
    if (!contributorTierColor || contributorTierRules.length === 0) {
      return fallback;
    }
    return { contributorTierColor, contributorTierRules };
  } catch (error) {
    core.warning(`failed to load ${policyPath}, using fallback policy: ${error.message}`);
    return fallback;
  }
}

const { contributorTierColor, contributorTierRules } = await loadContributorTierPolicy();
const contributorTierLabels = contributorTierRules.map((rule) => rule.label);

const managedPathLabels = [
  "docs",
  "dependencies",
  "ci",
  "core",
  "agent",
  "channel",
  "config",
  "cron",
  "daemon",
  "doctor",
  "gateway",
  "health",
  "heartbeat",
  "integration",
  "memory",
  "observability",
  "onboard",
  "provider",
  "runtime",
  "security",
  "service",
  "skillforge",
  "skills",
  "tool",
  "tunnel",
  "tests",
  "scripts",
  "dev",
];
const managedPathLabelSet = new Set(managedPathLabels);

const moduleNamespaceRules = [
  { root: "src/agent/", prefix: "agent", coreEntries: new Set(["mod.rs"]) },
  { root: "src/channels/", prefix: "channel", coreEntries: new Set(["mod.rs", "traits.rs"]) },
  { root: "src/config/", prefix: "config", coreEntries: new Set(["mod.rs", "schema.rs"]) },
  { root: "src/cron/", prefix: "cron", coreEntries: new Set(["mod.rs"]) },
  { root: "src/daemon/", prefix: "daemon", coreEntries: new Set(["mod.rs"]) },
  { root: "src/doctor/", prefix: "doctor", coreEntries: new Set(["mod.rs"]) },
  { root: "src/gateway/", prefix: "gateway", coreEntries: new Set(["mod.rs"]) },
  { root: "src/health/", prefix: "health", coreEntries: new Set(["mod.rs"]) },
  { root: "src/heartbeat/", prefix: "heartbeat", coreEntries: new Set(["mod.rs"]) },
  { root: "src/integrations/", prefix: "integration", coreEntries: new Set(["mod.rs", "registry.rs"]) },
  { root: "src/memory/", prefix: "memory", coreEntries: new Set(["mod.rs", "traits.rs"]) },
  { root: "src/observability/", prefix: "observability", coreEntries: new Set(["mod.rs", "traits.rs"]) },
  { root: "src/onboard/", prefix: "onboard", coreEntries: new Set(["mod.rs"]) },
  { root: "src/providers/", prefix: "provider", coreEntries: new Set(["mod.rs", "traits.rs"]) },
  { root: "src/runtime/", prefix: "runtime", coreEntries: new Set(["mod.rs", "traits.rs"]) },
  { root: "src/security/", prefix: "security", coreEntries: new Set(["mod.rs"]) },
  { root: "src/service/", prefix: "service", coreEntries: new Set(["mod.rs"]) },
  { root: "src/skillforge/", prefix: "skillforge", coreEntries: new Set(["mod.rs"]) },
  { root: "src/skills/", prefix: "skills", coreEntries: new Set(["mod.rs"]) },
  { root: "src/tools/", prefix: "tool", coreEntries: new Set(["mod.rs", "traits.rs"]) },
  { root: "src/tunnel/", prefix: "tunnel", coreEntries: new Set(["mod.rs"]) },
];
const managedModulePrefixes = [...new Set(moduleNamespaceRules.map((rule) => `${rule.prefix}:`))];
const orderedOtherLabelStyles = [
  { label: "health", color: "8EC9B8" },
  { label: "tool", color: "7FC4B6" },
  { label: "agent", color: "86C4A2" },
  { label: "memory", color: "8FCB99" },
  { label: "channel", color: "7EB6F2" },
  { label: "service", color: "95C7B6" },
  { label: "integration", color: "8DC9AE" },
  { label: "tunnel", color: "9FC8B3" },
  { label: "config", color: "AABCD0" },
  { label: "observability", color: "84C9D0" },
  { label: "docs", color: "8FBBE0" },
  { label: "dev", color: "B9C1CC" },
  { label: "tests", color: "9DC8C7" },
  { label: "skills", color: "BFC89B" },
  { label: "skillforge", color: "C9C39B" },
  { label: "provider", color: "958DF0" },
  { label: "runtime", color: "A3ADD8" },
  { label: "heartbeat", color: "C0C88D" },
  { label: "daemon", color: "C8C498" },
  { label: "doctor", color: "C1CF9D" },
  { label: "onboard", color: "D2BF86" },
  { label: "cron", color: "D2B490" },
  { label: "ci", color: "AEB4CE" },
  { label: "dependencies", color: "9FB1DE" },
  { label: "gateway", color: "B5A8E5" },
  { label: "security", color: "E58D85" },
  { label: "core", color: "C8A99B" },
  { label: "scripts", color: "C9B49F" },
];
const otherLabelDisplayOrder = orderedOtherLabelStyles.map((entry) => entry.label);
const modulePrefixSet = new Set(moduleNamespaceRules.map((rule) => rule.prefix));
const modulePrefixPriority = otherLabelDisplayOrder.filter((label) => modulePrefixSet.has(label));
const pathLabelPriority = [...otherLabelDisplayOrder];
const riskDisplayOrder = ["risk: high", "risk: medium", "risk: low", "risk: manual"];
const sizeDisplayOrder = ["size: XS", "size: S", "size: M", "size: L", "size: XL"];
const contributorDisplayOrder = [
  "distinguished contributor",
  "principal contributor",
  "experienced contributor",
  "trusted contributor",
];
const modulePrefixPriorityIndex = new Map(
  modulePrefixPriority.map((prefix, index) => [prefix, index])
);
const pathLabelPriorityIndex = new Map(
  pathLabelPriority.map((label, index) => [label, index])
);
const riskPriorityIndex = new Map(
  riskDisplayOrder.map((label, index) => [label, index])
);
const sizePriorityIndex = new Map(
  sizeDisplayOrder.map((label, index) => [label, index])
);
const contributorPriorityIndex = new Map(
  contributorDisplayOrder.map((label, index) => [label, index])
);

const otherLabelColors = Object.fromEntries(
  orderedOtherLabelStyles.map((entry) => [entry.label, entry.color])
);
const staticLabelColors = {
  "size: XS": "E7CDD3",
  "size: S": "E1BEC7",
  "size: M": "DBB0BB",
  "size: L": "D4A2AF",
  "size: XL": "CE94A4",
  "risk: low": "97D3A6",
  "risk: medium": "E4C47B",
  "risk: high": "E98E88",
  "risk: manual": "B7A4E0",
  ...otherLabelColors,
};
const staticLabelDescriptions = {
  "size: XS": "Auto size: <=80 non-doc changed lines.",
  "size: S": "Auto size: 81-250 non-doc changed lines.",
  "size: M": "Auto size: 251-500 non-doc changed lines.",
  "size: L": "Auto size: 501-1000 non-doc changed lines.",
  "size: XL": "Auto size: >1000 non-doc changed lines.",
  "risk: low": "Auto risk: docs/chore-only paths.",
  "risk: medium": "Auto risk: src/** or dependency/config changes.",
  "risk: high": "Auto risk: security/runtime/gateway/tools/workflows.",
  "risk: manual": "Maintainer override: keep selected risk label.",
  docs: "Auto scope: docs/markdown/template files changed.",
  dependencies: "Auto scope: dependency manifest/lock/policy changed.",
  ci: "Auto scope: CI/workflow/hook files changed.",
  core: "Auto scope: root src/*.rs files changed.",
  agent: "Auto scope: src/agent/** changed.",
  channel: "Auto scope: src/channels/** changed.",
  config: "Auto scope: src/config/** changed.",
  cron: "Auto scope: src/cron/** changed.",
  daemon: "Auto scope: src/daemon/** changed.",
  doctor: "Auto scope: src/doctor/** changed.",
  gateway: "Auto scope: src/gateway/** changed.",
  health: "Auto scope: src/health/** changed.",
  heartbeat: "Auto scope: src/heartbeat/** changed.",
  integration: "Auto scope: src/integrations/** changed.",
  memory: "Auto scope: src/memory/** changed.",
  observability: "Auto scope: src/observability/** changed.",
  onboard: "Auto scope: src/onboard/** changed.",
  provider: "Auto scope: src/providers/** changed.",
  runtime: "Auto scope: src/runtime/** changed.",
  security: "Auto scope: src/security/** changed.",
  service: "Auto scope: src/service/** changed.",
  skillforge: "Auto scope: src/skillforge/** changed.",
  skills: "Auto scope: src/skills/** changed.",
  tool: "Auto scope: src/tools/** changed.",
  tunnel: "Auto scope: src/tunnel/** changed.",
  tests: "Auto scope: tests/** changed.",
  scripts: "Auto scope: scripts/** changed.",
  dev: "Auto scope: dev/** changed.",
};
for (const label of contributorTierLabels) {
  staticLabelColors[label] = contributorTierColor;
  const rule = contributorTierRules.find((entry) => entry.label === label);
  if (rule) {
    staticLabelDescriptions[label] = `Contributor with ${rule.minMergedPRs}+ merged PRs.`;
  }
}

const modulePrefixColors = Object.fromEntries(
  modulePrefixPriority.map((prefix) => [
    `${prefix}:`,
    otherLabelColors[prefix] || "BFDADC",
  ])
);

const providerKeywordHints = [
  "deepseek",
  "moonshot",
  "kimi",
  "qwen",
  "mistral",
  "doubao",
  "baichuan",
  "yi",
  "siliconflow",
  "vertex",
  "azure",
  "perplexity",
  "venice",
  "vercel",
  "cloudflare",
  "synthetic",
  "opencode",
  "zai",
  "glm",
  "minimax",
  "bedrock",
  "qianfan",
  "groq",
  "together",
  "fireworks",
  "cohere",
  "openai",
  "openrouter",
  "anthropic",
  "gemini",
  "ollama",
];

const channelKeywordHints = [
  "telegram",
  "discord",
  "slack",
  "whatsapp",
  "matrix",
  "irc",
  "imessage",
  "email",
  "cli",
];

function isDocsLike(path) {
  return (
    path.startsWith("docs/") ||
    path.endsWith(".md") ||
    path.endsWith(".mdx") ||
    path === "LICENSE" ||
    path === ".markdownlint-cli2.yaml" ||
    path === ".github/pull_request_template.md" ||
    path.startsWith(".github/ISSUE_TEMPLATE/")
  );
}

function normalizeLabelSegment(segment) {
  return (segment || "")
    .toLowerCase()
    .replace(/\.rs$/g, "")
    .replace(/[^a-z0-9_-]+/g, "-")
    .replace(/^[-_]+|[-_]+$/g, "")
    .slice(0, 40);
}

function containsKeyword(text, keyword) {
  const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  const pattern = new RegExp(`(^|[^a-z0-9_])${escaped}([^a-z0-9_]|$)`, "i");
  return pattern.test(text);
}

function formatModuleLabel(prefix, segment) {
  return `${prefix}: ${segment}`;
}

function parseModuleLabel(label) {
  if (typeof label !== "string") return null;
  const match = label.match(/^([^:]+):\s*(.+)$/);
  if (!match) return null;
  const prefix = match[1].trim().toLowerCase();
  const segment = (match[2] || "").trim().toLowerCase();
  if (!prefix || !segment) return null;
  return { prefix, segment };
}

function sortByPriority(labels, priorityIndex) {
  return [...new Set(labels)].sort((left, right) => {
    const leftPriority = priorityIndex.has(left) ? priorityIndex.get(left) : Number.MAX_SAFE_INTEGER;
    const rightPriority = priorityIndex.has(right)
      ? priorityIndex.get(right)
      : Number.MAX_SAFE_INTEGER;
    if (leftPriority !== rightPriority) return leftPriority - rightPriority;
    return left.localeCompare(right);
  });
}

function sortModuleLabels(labels) {
  return [...new Set(labels)].sort((left, right) => {
    const leftParsed = parseModuleLabel(left);
    const rightParsed = parseModuleLabel(right);
    if (!leftParsed || !rightParsed) return left.localeCompare(right);

    const leftPrefixPriority = modulePrefixPriorityIndex.has(leftParsed.prefix)
      ? modulePrefixPriorityIndex.get(leftParsed.prefix)
      : Number.MAX_SAFE_INTEGER;
    const rightPrefixPriority = modulePrefixPriorityIndex.has(rightParsed.prefix)
      ? modulePrefixPriorityIndex.get(rightParsed.prefix)
      : Number.MAX_SAFE_INTEGER;

    if (leftPrefixPriority !== rightPrefixPriority) {
      return leftPrefixPriority - rightPrefixPriority;
    }
    if (leftParsed.prefix !== rightParsed.prefix) {
      return leftParsed.prefix.localeCompare(rightParsed.prefix);
    }

    const leftIsCore = leftParsed.segment === "core";
    const rightIsCore = rightParsed.segment === "core";
    if (leftIsCore !== rightIsCore) return leftIsCore ? 1 : -1;

    return leftParsed.segment.localeCompare(rightParsed.segment);
  });
}

function refineModuleLabels(rawLabels) {
  const refined = new Set(rawLabels);
  const segmentsByPrefix = new Map();

  for (const label of rawLabels) {
    const parsed = parseModuleLabel(label);
    if (!parsed) continue;
    if (!segmentsByPrefix.has(parsed.prefix)) {
      segmentsByPrefix.set(parsed.prefix, new Set());
    }
    segmentsByPrefix.get(parsed.prefix).add(parsed.segment);
  }

  for (const [prefix, segments] of segmentsByPrefix) {
    const hasSpecificSegment = [...segments].some((segment) => segment !== "core");
    if (hasSpecificSegment) {
      refined.delete(formatModuleLabel(prefix, "core"));
    }
  }

  return refined;
}

function compactModuleLabels(labels) {
  const groupedSegments = new Map();
  const compactedModuleLabels = new Set();
  const forcePathPrefixes = new Set();

  for (const label of labels) {
    const parsed = parseModuleLabel(label);
    if (!parsed) {
      compactedModuleLabels.add(label);
      continue;
    }
    if (!groupedSegments.has(parsed.prefix)) {
      groupedSegments.set(parsed.prefix, new Set());
    }
    groupedSegments.get(parsed.prefix).add(parsed.segment);
  }

  for (const [prefix, segments] of groupedSegments) {
    const uniqueSegments = [...new Set([...segments].filter(Boolean))];
    if (uniqueSegments.length === 0) continue;

    if (uniqueSegments.length === 1) {
      compactedModuleLabels.add(formatModuleLabel(prefix, uniqueSegments[0]));
    } else {
      forcePathPrefixes.add(prefix);
    }
  }

  return {
    moduleLabels: compactedModuleLabels,
    forcePathPrefixes,
  };
}

function colorForLabel(label) {
  if (staticLabelColors[label]) return staticLabelColors[label];
  const matchedPrefix = Object.keys(modulePrefixColors).find((prefix) => label.startsWith(prefix));
  if (matchedPrefix) return modulePrefixColors[matchedPrefix];
  return "BFDADC";
}

function descriptionForLabel(label) {
  if (staticLabelDescriptions[label]) return staticLabelDescriptions[label];

  const parsed = parseModuleLabel(label);
  if (parsed) {
    if (parsed.segment === "core") {
      return `Auto module: ${parsed.prefix} core files changed.`;
    }
    return `Auto module: ${parsed.prefix}/${parsed.segment} changed.`;
  }

  return "Auto-managed label.";
}

async function ensureLabel(name, existing = null) {
  const expectedColor = colorForLabel(name);
  const expectedDescription = descriptionForLabel(name);
  try {
    const current = existing || (await github.rest.issues.getLabel({ owner, repo, name })).data;
    const currentColor = (current.color || "").toUpperCase();
    const currentDescription = (current.description || "").trim();
    if (currentColor !== expectedColor || currentDescription !== expectedDescription) {
      await github.rest.issues.updateLabel({
        owner,
        repo,
        name,
        new_name: name,
        color: expectedColor,
        description: expectedDescription,
      });
    }
  } catch (error) {
    if (error.status !== 404) throw error;
    await github.rest.issues.createLabel({
      owner,
      repo,
      name,
      color: expectedColor,
      description: expectedDescription,
    });
  }
}

function isManagedLabel(label) {
  if (label === manualRiskOverrideLabel) return true;
  if (sizeLabels.includes(label) || computedRiskLabels.includes(label)) return true;
  if (managedPathLabelSet.has(label)) return true;
  if (contributorTierLabels.includes(label)) return true;
  if (managedModulePrefixes.some((prefix) => label.startsWith(prefix))) return true;
  return false;
}

async function ensureManagedRepoLabelsMetadata() {
  const repoLabels = await github.paginate(github.rest.issues.listLabelsForRepo, {
    owner,
    repo,
    per_page: 100,
  });

  for (const existingLabel of repoLabels) {
    const labelName = existingLabel.name || "";
    if (!isManagedLabel(labelName)) continue;
    await ensureLabel(labelName, existingLabel);
  }
}

function selectContributorTier(mergedCount) {
  const matchedTier = contributorTierRules.find((rule) => mergedCount >= rule.minMergedPRs);
  return matchedTier ? matchedTier.label : null;
}

if (context.eventName === "workflow_dispatch") {
  const mode = (context.payload.inputs?.mode || "audit").toLowerCase();
  const shouldRepair = mode === "repair";
  const repoLabels = await github.paginate(github.rest.issues.listLabelsForRepo, {
    owner,
    repo,
    per_page: 100,
  });

  let managedScanned = 0;
  const drifts = [];

  for (const existingLabel of repoLabels) {
    const labelName = existingLabel.name || "";
    if (!isManagedLabel(labelName)) continue;
    managedScanned += 1;

    const expectedColor = colorForLabel(labelName);
    const expectedDescription = descriptionForLabel(labelName);
    const currentColor = (existingLabel.color || "").toUpperCase();
    const currentDescription = (existingLabel.description || "").trim();
    if (currentColor !== expectedColor || currentDescription !== expectedDescription) {
      drifts.push({
        name: labelName,
        currentColor,
        expectedColor,
        currentDescription,
        expectedDescription,
      });
      if (shouldRepair) {
        await ensureLabel(labelName, existingLabel);
      }
    }
  }

  core.summary
    .addHeading("Managed Label Governance", 2)
    .addRaw(`Mode: ${shouldRepair ? "repair" : "audit"}`)
    .addEOL()
    .addRaw(`Managed labels scanned: ${managedScanned}`)
    .addEOL()
    .addRaw(`Drifts found: ${drifts.length}`)
    .addEOL();

  if (drifts.length > 0) {
    const sample = drifts.slice(0, 30).map((entry) => [
      entry.name,
      `${entry.currentColor} -> ${entry.expectedColor}`,
      `${entry.currentDescription || "(blank)"} -> ${entry.expectedDescription}`,
    ]);
    core.summary.addTable([
      [{ data: "Label", header: true }, { data: "Color", header: true }, { data: "Description", header: true }],
      ...sample,
    ]);
    if (drifts.length > sample.length) {
      core.summary
        .addRaw(`Additional drifts not shown: ${drifts.length - sample.length}`)
        .addEOL();
    }
  }

  await core.summary.write();

  if (!shouldRepair && drifts.length > 0) {
    core.info(`Managed-label metadata drifts detected: ${drifts.length}. Re-run with mode=repair to auto-fix.`);
  } else if (shouldRepair) {
    core.info(`Managed-label metadata repair applied to ${drifts.length} labels.`);
  } else {
    core.info("No managed-label metadata drift detected.");
  }

  return;
}

const files = await github.paginate(github.rest.pulls.listFiles, {
  owner,
  repo,
  pull_number: pr.number,
  per_page: 100,
});

const detectedModuleLabels = new Set();
for (const file of files) {
  const path = (file.filename || "").toLowerCase();
  for (const rule of moduleNamespaceRules) {
    if (!path.startsWith(rule.root)) continue;

    const relative = path.slice(rule.root.length);
    if (!relative) continue;

    const first = relative.split("/")[0];
    const firstStem = first.endsWith(".rs") ? first.slice(0, -3) : first;
    let segment = firstStem;

    if (rule.coreEntries.has(first) || rule.coreEntries.has(firstStem)) {
      segment = "core";
    }

    segment = normalizeLabelSegment(segment);
    if (!segment) continue;

    detectedModuleLabels.add(formatModuleLabel(rule.prefix, segment));
  }
}

const providerRelevantFiles = files.filter((file) => {
  const path = file.filename || "";
  return (
    path.startsWith("src/providers/") ||
    path.startsWith("src/integrations/") ||
    path.startsWith("src/onboard/") ||
    path.startsWith("src/config/")
  );
});

if (providerRelevantFiles.length > 0) {
  const searchableText = [
    pr.title || "",
    pr.body || "",
    ...providerRelevantFiles.map((file) => file.filename || ""),
    ...providerRelevantFiles.map((file) => file.patch || ""),
  ]
    .join("\n")
    .toLowerCase();

  for (const keyword of providerKeywordHints) {
    if (containsKeyword(searchableText, keyword)) {
      detectedModuleLabels.add(formatModuleLabel("provider", keyword));
    }
  }
}

const channelRelevantFiles = files.filter((file) => {
  const path = file.filename || "";
  return (
    path.startsWith("src/channels/") ||
    path.startsWith("src/onboard/") ||
    path.startsWith("src/config/")
  );
});

if (channelRelevantFiles.length > 0) {
  const searchableText = [
    pr.title || "",
    pr.body || "",
    ...channelRelevantFiles.map((file) => file.filename || ""),
    ...channelRelevantFiles.map((file) => file.patch || ""),
  ]
    .join("\n")
    .toLowerCase();

  for (const keyword of channelKeywordHints) {
    if (containsKeyword(searchableText, keyword)) {
      detectedModuleLabels.add(formatModuleLabel("channel", keyword));
    }
  }
}

const refinedModuleLabels = refineModuleLabels(detectedModuleLabels);
const compactedModuleState = compactModuleLabels(refinedModuleLabels);
const selectedModuleLabels = compactedModuleState.moduleLabels;
const forcePathPrefixes = compactedModuleState.forcePathPrefixes;
const modulePrefixesWithLabels = new Set(
  [...selectedModuleLabels]
    .map((label) => parseModuleLabel(label)?.prefix)
    .filter(Boolean)
);

const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
  owner,
  repo,
  issue_number: pr.number,
});
const currentLabelNames = currentLabels.map((label) => label.name);
const currentPathLabels = currentLabelNames.filter((label) => managedPathLabelSet.has(label));
const candidatePathLabels = new Set([...currentPathLabels, ...forcePathPrefixes]);

const dedupedPathLabels = [...candidatePathLabels].filter((label) => {
  if (label === "core") return true;
  if (forcePathPrefixes.has(label)) return true;
  return !modulePrefixesWithLabels.has(label);
});

const excludedLockfiles = new Set(["Cargo.lock"]);
const changedLines = files.reduce((total, file) => {
  const path = file.filename || "";
  if (isDocsLike(path) || excludedLockfiles.has(path)) {
    return total;
  }
  return total + (file.additions || 0) + (file.deletions || 0);
}, 0);

let sizeLabel = "size: XL";
if (changedLines <= 80) sizeLabel = "size: XS";
else if (changedLines <= 250) sizeLabel = "size: S";
else if (changedLines <= 500) sizeLabel = "size: M";
else if (changedLines <= 1000) sizeLabel = "size: L";

const hasHighRiskPath = files.some((file) => {
  const path = file.filename || "";
  return (
    path.startsWith("src/security/") ||
    path.startsWith("src/runtime/") ||
    path.startsWith("src/gateway/") ||
    path.startsWith("src/tools/") ||
    path.startsWith(".github/workflows/")
  );
});

const hasMediumRiskPath = files.some((file) => {
  const path = file.filename || "";
  return (
    path.startsWith("src/") ||
    path === "Cargo.toml" ||
    path === "Cargo.lock" ||
    path === "deny.toml" ||
    path.startsWith(".githooks/")
  );
});

let riskLabel = "risk: low";
if (hasHighRiskPath) {
  riskLabel = "risk: high";
} else if (hasMediumRiskPath) {
  riskLabel = "risk: medium";
}

await ensureManagedRepoLabelsMetadata();

const labelsToEnsure = new Set([
  ...sizeLabels,
  ...computedRiskLabels,
  manualRiskOverrideLabel,
  ...managedPathLabels,
  ...contributorTierLabels,
  ...selectedModuleLabels,
]);

for (const label of labelsToEnsure) {
  await ensureLabel(label);
}

let contributorTierLabel = null;
const authorLogin = pr.user?.login;
if (authorLogin && pr.user?.type !== "Bot") {
  try {
    const { data: mergedSearch } = await github.rest.search.issuesAndPullRequests({
      q: `repo:${owner}/${repo} is:pr is:merged author:${authorLogin}`,
      per_page: 1,
    });
    const mergedCount = mergedSearch.total_count || 0;
    contributorTierLabel = selectContributorTier(mergedCount);
  } catch (error) {
    core.warning(`failed to compute contributor tier label: ${error.message}`);
  }
}

const hasManualRiskOverride = currentLabelNames.includes(manualRiskOverrideLabel);
const keepNonManagedLabels = currentLabelNames.filter((label) => {
  if (label === manualRiskOverrideLabel) return true;
  if (contributorTierLabels.includes(label)) return false;
  if (sizeLabels.includes(label) || computedRiskLabels.includes(label)) return false;
  if (managedPathLabelSet.has(label)) return false;
  if (managedModulePrefixes.some((prefix) => label.startsWith(prefix))) return false;
  return true;
});

const manualRiskSelection =
  currentLabelNames.find((label) => computedRiskLabels.includes(label)) || riskLabel;

const moduleLabelList = sortModuleLabels([...selectedModuleLabels]);
const contributorLabelList = contributorTierLabel ? [contributorTierLabel] : [];
const selectedRiskLabels = hasManualRiskOverride
  ? sortByPriority([manualRiskSelection, manualRiskOverrideLabel], riskPriorityIndex)
  : sortByPriority([riskLabel], riskPriorityIndex);
const selectedSizeLabels = sortByPriority([sizeLabel], sizePriorityIndex);
const sortedContributorLabels = sortByPriority(contributorLabelList, contributorPriorityIndex);
const sortedPathLabels = sortByPriority(dedupedPathLabels, pathLabelPriorityIndex);
const sortedKeepNonManagedLabels = [...new Set(keepNonManagedLabels)].sort((left, right) =>
  left.localeCompare(right)
);

const nextLabels = [
  ...new Set([
    ...selectedRiskLabels,
    ...selectedSizeLabels,
    ...sortedContributorLabels,
    ...moduleLabelList,
    ...sortedPathLabels,
    ...sortedKeepNonManagedLabels,
  ]),
];

await github.rest.issues.setLabels({
  owner,
  repo,
  issue_number: pr.number,
  labels: nextLabels,
});
};
