const vscode = require('vscode');
const fs     = require('fs');
const path   = require('path');
const cp     = require('child_process');   // ← needed for isSwagAvailable

// ═══════════════════════════════════════════════════════════════════════════════
// ENTRY POINTS
// ═══════════════════════════════════════════════════════════════════════════════

function activate(context) {
  context.subscriptions.push(
    vscode.commands.registerCommand('astroswagger.generateSwagger', cmdGenerateSwagger),
    vscode.commands.registerCommand('astroswagger.openDocumentation', cmdOpenDocumentation),
  );
}

function deactivate() {}

async function cmdGenerateSwagger() {
  const editor = vscode.window.activeTextEditor;
  if (!editor) {
    vscode.window.showErrorMessage('astroswagger: No file is currently open.');
    return;
  }

  const filePath = editor.document.uri.fsPath;
  if (!filePath.endsWith('.go')) {
    vscode.window.showWarningMessage('astroswagger: Only works on .go files.');
    return;
  }

  const folders = vscode.workspace.workspaceFolders;
  if (!folders) {
    vscode.window.showErrorMessage('astroswagger: No workspace folder open.');
    return;
  }

  const rootPath   = folders[0].uri.fsPath;
  const moduleName = path.basename(path.dirname(filePath));

  // ── Step 1: build route map ─────────────────────────────────────────────────
  const allGoFiles     = collectGoFiles(rootPath);
  const globalRouteMap = buildRouteMap(allGoFiles);

  if (Object.keys(globalRouteMap).length === 0) {
    vscode.window.showWarningMessage('astroswagger: No gin routes found in this project.');
    return;
  }

  // ── Step 2: inject annotations ──────────────────────────────────────────────
  const updated = injectAnnotations(filePath, globalRouteMap, moduleName);
  await vscode.commands.executeCommand('workbench.action.files.revert');

  vscode.window.showInformationMessage(
    `✅ astroswagger: ${updated} annotation(s) updated in ${path.basename(filePath)}`
  );

  // ── Step 3: run swag fmt && swag init ───────────────────────────────────────
  await runSwagCommands(rootPath);
}

function cmdOpenDocumentation() {
  vscode.env.openExternal(vscode.Uri.parse('https://github.com/yassine-manai/VS-Code-Swagger-Generator-for-Golang-Project#readme'));
}

// ═══════════════════════════════════════════════════════════════════════════════
// SWAG COMMANDS
// ═══════════════════════════════════════════════════════════════════════════════

/**
 * Run `swag fmt` then `swag init` sequentially in the workspace root.
 *
 * Uses VS Code's integrated terminal so the user sees full output and errors.
 * Reuses an existing astroswagger terminal if one is already open.
 *
 * @param {string} cwd  — workspace root path
 */
async function runSwagCommands(cwd) {
  const swagAvailable = await isSwagAvailable(cwd);

  if (!swagAvailable) {
    const choice = await vscode.window.showWarningMessage(
      'astroswagger: `swag` CLI not found. Install it to auto-regenerate docs.',
      'Install Guide',
      'Skip'
    );
    if (choice === 'Install Guide') {
      vscode.env.openExternal(vscode.Uri.parse('https://github.com/swaggo/swag#getting-started'));
    }
    return;
  }

  // Reuse or create a dedicated terminal so we don't spam new panels
  let terminal = vscode.window.terminals.find(t => t.name === 'astroswagger');
  if (!terminal) {
    terminal = vscode.window.createTerminal({ name: 'astroswagger', cwd });
  }

  terminal.show(true);  // open panel but keep editor focus
  terminal.sendText(`cd "${cwd}" && swag fmt && swag init`);
}

/**
 * Check whether the `swag` binary is on PATH by probing `swag --version`.
 *
 * @param {string} cwd
 * @returns {Promise<boolean>}
 */
function isSwagAvailable(cwd) {
  return new Promise(resolve => {
    cp.exec('swag --version', { cwd }, err => resolve(!err));
  });
}

// ═══════════════════════════════════════════════════════════════════════════════
// PHASE 0 — FILE COLLECTION
// ═══════════════════════════════════════════════════════════════════════════════

/**
 * Recursively collect all .go files under rootPath,
 * skipping hidden directories and the vendor folder.
 *
 * @param {string} rootPath
 * @returns {string[]}
 */
function collectGoFiles(rootPath) {
  const files = [];

  function scan(dir) {
    let entries;
    try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
    catch { return; }

    for (const entry of entries) {
      if (entry.isDirectory()) {
        if (!entry.name.startsWith('.') && entry.name !== 'vendor') {
          scan(path.join(dir, entry.name));
        }
      } else if (entry.isFile() && entry.name.endsWith('.go')) {
        files.push(path.join(dir, entry.name));
      }
    }
  }

  scan(rootPath);
  return files;
}

// ═══════════════════════════════════════════════════════════════════════════════
// PHASE 1 — PARSE ALL FILES INTO A CALL GRAPH
// ═══════════════════════════════════════════════════════════════════════════════

/**
 * A FuncInfo describes everything we need to know about one Go function:
 *
 *   params     — list of parameter names (positional)
 *   groups     — local Group() assignments: varName -> { parent, path }
 *   engineVars — vars assigned from gin.New() / gin.Default()
 *   aliases    — simple assignments between vars: varName -> sourceVar
 *   routes     — HTTP route registrations found inside this function
 *   calls      — calls to other functions that receive a gin var
 *
 * @typedef {{ parent: string, path: string }} GroupDef
 * @typedef {{ ginVar: string, method: string, routePath: string, handler: string }} RouteDef
 * @typedef {{ callee: string, ginArgIndex: number, ginVarName: string }} CallDef
 *
 * @typedef {{
 *   params:      string[],
 *   groups:      Record<string, GroupDef>,
 *   engineVars:  string[],
 *   aliases:     Record<string, string>,
 *   routes:      RouteDef[],
 *   calls:       CallDef[],
 * }} FuncInfo
 */

/**
 * Parse every .go file and build a map of funcName -> FuncInfo.
 *
 * @param {string[]} files
 * @returns {Record<string, FuncInfo>}
 */
function parseCallGraph(files) {
  /** @type {Record<string, FuncInfo>} */
  const graph = {};

  for (const file of files) {
    let raw;
    try { raw = fs.readFileSync(file, 'utf8'); }
    catch { continue; }

    // Strip // line comments to avoid false regex matches inside comments.
    const src = raw.replace(/\/\/[^\n]*/g, '');
    parseFunctions(src, graph);
  }

  return graph;
}

/**
 * Extract all top-level functions from a source string and populate graph.
 *
 * @param {string} src    — comment-stripped Go source
 * @param {Record<string, FuncInfo>} graph
 */
function parseFunctions(src, graph) {
  // Match both plain functions and method receivers:
  //   func FuncName(...)
  //   func (recv Type) FuncName(...)
  const funcDeclRe = /\bfunc\s+(?:\([^)]*\)\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*\(([^)]*)\)/g;
  const positions  = [];
  let m;

  while ((m = funcDeclRe.exec(src)) !== null) {
    positions.push({ name: m[1], paramStr: m[2], start: m.index });
  }

  for (let i = 0; i < positions.length; i++) {
    const { name, paramStr, start } = positions[i];
    const end  = positions[i + 1] ? positions[i + 1].start : src.length;
    const body = src.slice(start, end);

    const params     = parseParamNames(paramStr);
    const groups     = parseGroupDefs(body);
    const engineVars = parseEngineVars(body);
    const aliases    = parseAliases(body);
    const routes     = parseRoutes(body);
    const calls      = parseCalls(body);

    if (graph[name]) {
      graph[name].routes.push(...routes);
      graph[name].calls.push(...calls);
      graph[name].engineVars.push(...engineVars);
      Object.assign(graph[name].groups,  groups);
      Object.assign(graph[name].aliases, aliases);
    } else {
      graph[name] = { params, groups, engineVars, aliases, routes, calls };
    }
  }
}

// ─── Parameter names ──────────────────────────────────────────────────────────

/**
 * Extract ordered parameter names from a Go parameter list string.
 * "r *gin.RouterGroup, db *bun.DB"  →  ["r", "db"]
 *
 * @param {string} paramStr
 * @returns {string[]}
 */
function parseParamNames(paramStr) {
  if (!paramStr.trim()) return [];
  return paramStr
    .split(',')
    .map(p => p.trim().split(/\s+/)[0])
    .filter(n => n && /^[A-Za-z_]/.test(n));
}

// ─── Group definitions ────────────────────────────────────────────────────────

/**
 * Find all `varName := parentVar.Group("/path")` patterns.
 * Extra args after the path (middleware) are ignored.
 *
 * @param {string} body
 * @returns {Record<string, GroupDef>}
 */
function parseGroupDefs(body) {
  const groups = {};
  const re = /([A-Za-z_][A-Za-z0-9_]*)\s*:?=\s*([A-Za-z_][A-Za-z0-9_]*)\.Group\s*\(\s*"([^"]+)"/g;
  let m;
  while ((m = re.exec(body)) !== null) {
    groups[m[1]] = { parent: m[2], path: m[3] };
  }
  return groups;
}

// ─── Engine var assignments ───────────────────────────────────────────────────

/**
 * Find variables assigned from gin.New() or gin.Default().
 * These are the root gin vars seeded with prefix "".
 *
 * @param {string} body
 * @returns {string[]}
 */
function parseEngineVars(body) {
  const vars = [];
  const re   = /([A-Za-z_][A-Za-z0-9_]*)\s*:?=\s*gin\s*\.\s*(?:New|Default)\s*\(\s*\)/g;
  let m;
  while ((m = re.exec(body)) !== null) {
    vars.push(m[1]);
  }
  return vars;
}

// ─── Aliases ──────────────────────────────────────────────────────────────────

/**
 * Find simple var-to-var assignments that could alias a gin group:
 *   api := v1  |  protected = r
 *
 * @param {string} body
 * @returns {Record<string, string>}
 */
function parseAliases(body) {
  const aliases = {};
  const re = /([A-Za-z_][A-Za-z0-9_]*)\s*:?=\s*([A-Za-z_][A-Za-z0-9_]*)\s*(?:[^.(]|$)/gm;
  let m;
  while ((m = re.exec(body)) !== null) {
    aliases[m[1]] = m[2];
  }
  return aliases;
}

// ─── Route registrations ──────────────────────────────────────────────────────

const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'Any'];

/**
 * Find all HTTP route registrations:
 *   ginVar.GET("/path", [...pkg.]HandlerFunc)
 *
 * Anonymous handlers func(c *gin.Context) are skipped.
 *
 * @param {string} body
 * @returns {RouteDef[]}
 */
function parseRoutes(body) {
  const routes        = [];
  const methodPattern = HTTP_METHODS.join('|');
  const re = new RegExp(
    `([A-Za-z_][A-Za-z0-9_]*)\\.(${methodPattern})\\s*\\(\\s*"([^"]+)"\\s*,\\s*([^)]+)`,
    'g'
  );

  let m;
  while ((m = re.exec(body)) !== null) {
    const ginVar     = m[1];
    const method     = m[2].toLowerCase();
    const routePath  = m[3];
    const handlerRaw = m[4].trim();

    if (handlerRaw.startsWith('func')) continue;

    const handlerMatch = handlerRaw.match(/(?:[A-Za-z_][A-Za-z0-9_]*\.)*([A-Za-z_][A-Za-z0-9_]*)/);
    if (!handlerMatch) continue;

    routes.push({ ginVar, method, routePath, handler: handlerMatch[1] });
  }

  return routes;
}

// ─── Sub-router calls ─────────────────────────────────────────────────────────

/**
 * Find any function call and record each simple-identifier argument
 * with its index — the BFS will filter to known gin vars later.
 *
 * @param {string} body
 * @returns {CallDef[]}
 */
function parseCalls(body) {
  const calls = [];
  const re    = /(?:[A-Za-z_][A-Za-z0-9_]*\.)?([A-Za-z_][A-Za-z0-9_]*)\s*\(([^)]*)\)/g;
  let m;

  while ((m = re.exec(body)) !== null) {
    const callee = m[1];
    const argStr = m[2].trim();
    if (!argStr) continue;

    const args = splitArgs(argStr);
    for (let i = 0; i < args.length; i++) {
      const arg = args[i].trim();
      if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(arg)) {
        calls.push({ callee, ginArgIndex: i, ginVarName: arg });
      }
    }
  }

  return calls;
}

/**
 * Split a comma-separated argument string, respecting nested parens.
 *
 * @param {string} str
 * @returns {string[]}
 */
function splitArgs(str) {
  const args  = [];
  let depth   = 0;
  let current = '';

  for (const ch of str) {
    if (ch === '(' || ch === '[' || ch === '{') { depth++; current += ch; }
    else if (ch === ')' || ch === ']' || ch === '}') { depth--; current += ch; }
    else if (ch === ',' && depth === 0) { args.push(current); current = ''; }
    else { current += ch; }
  }

  if (current.trim()) args.push(current);
  return args;
}

// ═══════════════════════════════════════════════════════════════════════════════
// PHASE 2 — RESOLVE PREFIXES VIA BFS
// ═══════════════════════════════════════════════════════════════════════════════

/**
 * Parse all files, walk the call graph, and return a flat map of:
 *   handlerName -> { method, route }
 *
 * @param {string[]} allGoFiles
 * @returns {Record<string, { method: string, route: string }>}
 */
function buildRouteMap(allGoFiles) {
  const graph = parseCallGraph(allGoFiles);

  /** @type {Record<string, Record<string, string>>} */
  const resolvedVars = {};

  // ── Seed root functions ──────────────────────────────────────────────────────
  const calledFuncs = new Set();
  for (const info of Object.values(graph)) {
    for (const call of info.calls) calledFuncs.add(call.callee);
  }

  for (const [funcName, info] of Object.entries(graph)) {
    if ((info.engineVars || []).length > 0) {
      // Has gin.New() / gin.Default() — definitive root
      resolvedVars[funcName] = buildLocalVars({}, info);
    } else if (!calledFuncs.has(funcName) && Object.keys(info.groups).length > 0) {
      // Never called + has groups — treat as entry point
      const lv = buildLocalVars({}, info);
      if (Object.keys(lv).length > 0) resolvedVars[funcName] = lv;
    }
  }

  // ── BFS: propagate prefixes down the call chain ──────────────────────────────
  const queue   = Object.keys(resolvedVars);
  const visited = new Set(queue);

  while (queue.length > 0) {
    const callerName = queue.shift();
    const callerInfo = graph[callerName];
    if (!callerInfo) continue;

    const localVars = buildLocalVars(resolvedVars[callerName] || {}, callerInfo);

    for (const call of callerInfo.calls) {
      if (!(call.ginVarName in localVars)) continue;

      const passedPrefix = localVars[call.ginVarName];
      const calleeInfo   = graph[call.callee];
      if (!calleeInfo) continue;

      const paramName = calleeInfo.params[call.ginArgIndex];
      if (!paramName) continue;

      if (!visited.has(call.callee)) {
        resolvedVars[call.callee] = { [paramName]: passedPrefix };
        visited.add(call.callee);
        queue.push(call.callee);
      } else {
        resolvedVars[call.callee] = resolvedVars[call.callee] || {};
        if (!(paramName in resolvedVars[call.callee])) {
          resolvedVars[call.callee][paramName] = passedPrefix;
        }
      }
    }
  }

  // ── Collect routes ───────────────────────────────────────────────────────────
  /** @type {Record<string, { method: string, route: string }>} */
  const routeMap = {};

  for (const [funcName, info] of Object.entries(graph)) {
    if (info.routes.length === 0) continue;

    const localVars = buildLocalVars(resolvedVars[funcName] || {}, info);

    for (const route of info.routes) {
      const prefix    = localVars[route.ginVar] ?? '';
      const fullRoute = joinPaths(prefix, route.routePath);
      routeMap[route.handler] = { method: route.method, route: fullRoute };
    }
  }

  return routeMap;
}

// ─── Local var resolution ─────────────────────────────────────────────────────

/**
 * Build the complete gin-var map for a function.
 *
 * @param {Record<string, string>} seedVars  — params already resolved by BFS
 * @param {FuncInfo} info
 * @returns {Record<string, string>}
 */
function buildLocalVars(seedVars, info) {
  const vars = { ...seedVars };

  // Seed gin.New() / gin.Default() vars with prefix ""
  for (const varName of (info.engineVars || [])) {
    if (!(varName in vars)) vars[varName] = '';
  }

  // Fallback: if a Group()'s parent is unknown, treat it as a root engine var
  for (const def of Object.values(info.groups)) {
    if (!(def.parent in vars) && !(def.parent in info.groups)) {
      vars[def.parent] = '';
    }
  }

  // Iteratively resolve groups and aliases until stable
  const MAX_PASSES = 30;
  for (let pass = 0; pass < MAX_PASSES; pass++) {
    let changed = false;

    for (const [varName, def] of Object.entries(info.groups)) {
      if (varName in vars) continue;
      if (def.parent in vars) {
        vars[varName] = joinPaths(vars[def.parent], def.path);
        changed = true;
      }
    }

    for (const [varName, source] of Object.entries(info.aliases)) {
      if (varName in vars) continue;
      if (source in vars) {
        vars[varName] = vars[source];
        changed = true;
      }
    }

    if (!changed) break;
  }

  return vars;
}

// ═══════════════════════════════════════════════════════════════════════════════
// PHASE 3 — ANNOTATION INJECTION
// ═══════════════════════════════════════════════════════════════════════════════

/**
 * Read a handler .go file, inject or replace swagger godoc blocks above
 * every exported handler function that exists in routeMap.
 *
 * @param {string} filePath
 * @param {Record<string, { method: string, route: string }>} routeMap
 * @param {string} moduleName
 * @returns {number} number of annotations written
 */
function injectAnnotations(filePath, routeMap, moduleName) {
  const content = fs.readFileSync(filePath, 'utf8');
  const lines   = content.split('\n');
  const output  = [];
  let updated   = 0;
  let i         = 0;

  while (i < lines.length) {
    const line      = lines[i];
    const funcMatch = line.match(/^func\s+(?:\([^)]+\)\s+)?([A-Z][A-Za-z0-9_]*)\s*\(/);

    if (funcMatch) {
      const funcName  = funcMatch[1];
      const routeInfo = routeMap[funcName];

      // Strip any existing swagger comment block directly above this function
      while (output.length > 0 && output[output.length - 1].trim().startsWith('//')) {
        output.pop();
      }

      if (routeInfo) {
        const body       = extractFuncBody(content, funcName);
        const annotation = buildAnnotation(funcName, moduleName, routeInfo, body);
        output.push(...annotation.split('\n'));
        updated++;
      }
    }

    output.push(line);
    i++;
  }

  fs.writeFileSync(filePath, output.join('\n'), 'utf8');
  return updated;
}

// ─── Extract function body ────────────────────────────────────────────────────

/**
 * Extract the full source body of a named function by counting braces.
 *
 * @param {string} src
 * @param {string} funcName
 * @returns {string}
 */
function extractFuncBody(src, funcName) {
  const re = new RegExp(
    `\\bfunc\\s+(?:\\([^)]+\\)\\s+)?${funcName}\\s*\\([^)]*\\)[^{]*\\{`, 'g'
  );
  const match = re.exec(src);
  if (!match) return '';

  let depth = 0;
  let i     = match.index + match[0].length - 1;

  while (i < src.length) {
    if (src[i] === '{')      depth++;
    else if (src[i] === '}') { depth--; if (depth === 0) break; }
    i++;
  }

  return src.slice(match.index, i + 1);
}

// ─── Param extraction ─────────────────────────────────────────────────────────

/**
 * Extract :param segments from a route path.
 *
 * @param {string} route
 * @returns {Array<{ name: string, goType: string, required: boolean }>}
 */
function extractPathParams(route) {
  return [...route.matchAll(/:([a-zA-Z0-9_]+)/g)].map(m => ({
    name: m[1], goType: 'string', required: true,
  }));
}

/**
 * Extract query params from c.Query() and c.DefaultQuery() calls.
 *
 * @param {string} body
 * @returns {Array<{ name: string, goType: string, required: boolean, defaultVal: string }>}
 */
function extractQueryParams(body) {
  const params = [];
  const seen   = new Set();
  let match;

  const dqRe = /c\.DefaultQuery\s*\(\s*"([^"]+)"\s*,\s*"([^"]*)"\s*\)/g;
  while ((match = dqRe.exec(body)) !== null) {
    const name = match[1], def = match[2];
    if (seen.has(name)) continue;
    seen.add(name);
    const goType = (def !== '' && !isNaN(Number(def))) ? 'int' : 'string';
    params.push({ name, goType, required: false, defaultVal: def });
  }

  const qRe = /c\.Query\s*\(\s*"([^"]+)"\s*\)/g;
  while ((match = qRe.exec(body)) !== null) {
    const name = match[1];
    if (seen.has(name)) continue;
    seen.add(name);
    params.push({ name, goType: 'string', required: false, defaultVal: '' });
  }

  return params;
}

/**
 * Detect request body binding and extract the struct type name.
 *
 * Handles:
 *   var request CreateXxxRequest  + bind call
 *   request := CreateXxxRequest{} + bind call
 *   c.ShouldBindJSON(&req)        — fallback lookup
 *
 * @param {string} body
 * @returns {string|null}
 */
function extractBodyParam(body) {
  let m;

  const varDeclRe = /\bvar\s+([A-Za-z_][A-Za-z0-9_]*)\s+([A-Z][A-Za-z0-9_]*)\b/g;
  while ((m = varDeclRe.exec(body)) !== null) {
    if (isUsedInBind(body, m[1])) return m[2];
  }

  const shortDeclRe = /([A-Za-z_][A-Za-z0-9_]*)\s*:=\s*([A-Z][A-Za-z0-9_]*)\s*\{/g;
  while ((m = shortDeclRe.exec(body)) !== null) {
    if (isUsedInBind(body, m[1])) return m[2];
  }

  const bindRe = /(?:BindJSON|ShouldBindJSON|ShouldBind|BindWith|Bind)\s*\(\s*(?:c\s*,\s*)?&([A-Za-z_][A-Za-z0-9_]*)/g;
  while ((m = bindRe.exec(body)) !== null) {
    const typeRe = new RegExp(`\\b${m[1]}\\b[^=]*[:=][^=]\\s*([A-Z][A-Za-z0-9_]*)`, 'g');
    const tm = typeRe.exec(body);
    if (tm) return tm[1];
  }

  return null;
}

/**
 * Returns true if varName appears as an argument to a bind/decode call.
 *
 * @param {string} body
 * @param {string} varName
 * @returns {boolean}
 */
function isUsedInBind(body, varName) {
  return new RegExp(
    `(?:BindJSON|ShouldBindJSON|ShouldBind|BindWith|Bind|Decode)\\s*\\([^)]*&${varName}\\b`
  ).test(body);
}

// ═══════════════════════════════════════════════════════════════════════════════
// PHASE 4 — ANNOTATION GENERATION  (static inference)
// ═══════════════════════════════════════════════════════════════════════════════

/**
 * Build a complete swaggo godoc annotation block for one handler.
 * All content is statically inferred — instant, no API calls.
 *
 * @param {string} funcName
 * @param {string} moduleName
 * @param {{ method: string, route: string }} routeInfo
 * @param {string} body   — raw handler function source
 * @returns {string}
 */
function buildAnnotation(funcName, moduleName, { method, route }, body) {
  const swaggerRoute = route.replace(/:([a-zA-Z0-9_]+)/g, '{$1}');
  const pathParams   = extractPathParams(route);
  const queryParams  = extractQueryParams(body);
  const bodyType     = extractBodyParam(body);

  const summary     = staticSummary(funcName, method, route);
  const description = staticDescription(funcName, method, route, pathParams, queryParams);

  const pathLines = pathParams.map(p =>
    `//\t@Param\t\t\t${p.name}\tpath\t${p.goType}\ttrue\t"${p.name}"`
  );
  const queryLines = queryParams.map(p => {
    const def = p.defaultVal !== '' ? `\tdefault(${p.defaultVal})` : '';
    return `//\t@Param\t\t\t${p.name}\tquery\t${p.goType}\tfalse\t"${p.name}"${def}`;
  });
  const bodyLine = bodyType
    ? `//\t@Param\t\t\tbody\tbody\t${bodyType}\ttrue\t"Request body"`
    : null;

  const paramLines = [...pathLines, ...queryLines, bodyLine].filter(Boolean).join('\n');

  return [
    `// ${funcName} godoc`,
    `//`,
    `//\t@Summary\t\t${summary}`,
    `//\t@Description\t${description}`,
    `//\t@Tags\t\t\t${moduleName}`,
    `//\t@Accept\t\t\tjson`,
    `//\t@Produce\t\tjson`,
    paramLines || null,
    `//\t@Security\t\tBearer`,
    `//\t@Success\t\t200\t{object}\tinterface{}`,
    `//\t@Failure\t\t400\t{object}\tutils.Response`,
    `//\t@Failure\t\t500\t{object}\tutils.Response`,
    `//\t@Router\t\t\t${swaggerRoute} [${method}]`,
  ].filter(Boolean).join('\n');
}

// ─── Static summary ───────────────────────────────────────────────────────────

/**
 * Generate a human-readable one-line summary from the function name and route.
 *
 * GetAuditByIDHandler  GET  /audits/:id     →  "Retrieve audit by id"
 * ExportAuditHandler   POST /audits/export  →  "Export audit"
 * ListUsersHandler     GET  /users          →  "List users"
 *
 * @param {string} funcName
 * @param {string} method
 * @param {string} route
 * @returns {string}
 */
function staticSummary(funcName, method, route) {
  const noiseWords = new Set(['handler', 'controller', 'api', 'endpoint', 'func', 'action']);
  const tokens     = splitCamelCase(funcName).filter(t => !noiseWords.has(t.toLowerCase()));

  const verbMap = {
    get:      'Retrieve', fetch:    'Retrieve', find:     'Retrieve', read:    'Retrieve',
    list:     'List',     getall:   'List',     fetchall: 'List',
    create:   'Create',   add:      'Create',   new:      'Create',   register: 'Create',
    update:   'Update',   edit:     'Update',   modify:   'Update',   patch:    'Update',
    delete:   'Delete',   remove:   'Delete',   destroy:  'Delete',
    export:   'Export',   import:   'Import',
    upload:   'Upload',   download: 'Download',
    send:     'Send',     check:    'Check',
    validate: 'Validate', search:   'Search',   count:    'Count',
    login:    'Login',    logout:   'Logout',   refresh:  'Refresh',
    verify:   'Verify',   approve:  'Approve',  reject:   'Reject',
    assign:   'Assign',   revoke:   'Revoke',
  };

  const methodFallback = {
    get: 'Retrieve', post: 'Create', put: 'Update', patch: 'Update', delete: 'Delete',
  };

  let action         = '';
  let resourceTokens = tokens;

  if (tokens.length > 0) {
    const first = tokens[0].toLowerCase();
    if (verbMap[first]) {
      action         = verbMap[first];
      resourceTokens = tokens.slice(1);
    }
  }

  if (!action) action = methodFallback[method] || 'Process';

  let resource  = '';
  let qualifier = '';

  for (let i = 0; i < resourceTokens.length; i++) {
    const t = resourceTokens[i];
    if (t.toLowerCase() === 'by' && i + 1 < resourceTokens.length) {
      qualifier = 'by ' + resourceTokens.slice(i + 1).join(' ').toLowerCase();
      break;
    }
    resource += (resource ? ' ' : '') + t.toLowerCase();
  }

  if (!qualifier) {
    const pathParams = [...route.matchAll(/:([a-zA-Z0-9_]+)/g)].map(m => m[1]);
    if (pathParams.length > 0) {
      qualifier = 'by ' + pathParams[pathParams.length - 1].replace(/_/g, ' ');
    }
  }

  return [action, resource, qualifier].filter(Boolean).join(' ').trim() || funcName;
}

/**
 * Split a camelCase or PascalCase identifier into words.
 * "GetAuditByIDHandler" → ["Get", "Audit", "By", "ID"]
 *
 * @param {string} name
 * @returns {string[]}
 */
function splitCamelCase(name) {
  return name
    .replace(/([A-Z][a-z])/g,        ' $1')
    .replace(/([a-z])([A-Z])/g,      '$1 $2')
    .replace(/([A-Z]+)([A-Z][a-z])/g,'$1 $2')
    .trim()
    .split(/\s+/)
    .filter(Boolean);
}

// ─── Static description ───────────────────────────────────────────────────────

/**
 * Generate a full-sentence description combining the summary with
 * path param requirements and query filter names.
 *
 * @param {string} funcName
 * @param {string} method
 * @param {string} route
 * @param {Array}  pathParams
 * @param {Array}  queryParams
 * @returns {string}
 */
function staticDescription(funcName, method, route, pathParams, queryParams) {
  const parts = [staticSummary(funcName, method, route) + '.'];

  if (pathParams.length > 0) {
    parts.push(`Requires path parameter(s): ${pathParams.map(p => p.name).join(', ')}.`);
  }

  if (queryParams.length > 0) {
    parts.push(`Supports query filters: ${queryParams.map(p => p.name).join(', ')}.`);
  }

  parts.push('Requires Bearer authentication.');
  return parts.join(' ');
}

// ═══════════════════════════════════════════════════════════════════════════════
// UTILITIES
// ═══════════════════════════════════════════════════════════════════════════════

/**
 * Join URL path segments with exactly one slash between each,
 * and a leading slash on the result.
 *
 * @param {...string} parts
 * @returns {string}
 */
function joinPaths(...parts) {
  const joined = parts
    .map(p => (p || '').replace(/^\/+|\/+$/g, ''))
    .filter(Boolean)
    .join('/');
  return joined ? '/' + joined : '/';
}

// ═══════════════════════════════════════════════════════════════════════════════

module.exports = { activate, deactivate };