/**
 * Flags webhook PARSE handlers that have no signature-verification import.
 * Webhook payloads are provider-controlled untrusted strings; the default
 * generated shape is "parse the body, return impactedItems" which silently
 * drops the verifier step.
 *
 * Scope (any of):
 *   - filename matches /webhookParse\.ts$/i, /parseWebhooks?\.ts$/i,
 *     /webhooksParse\.ts$/i
 *   - any file that imports `ParseWebhooksHandler` from `@unito/integration-sdk`
 *     or `@unito/integration-api`
 *
 * `AcknowledgeWebhooksHandler` exports are out of scope (handshake response,
 * not payload parser).
 *
 * Opt-out: JSDoc `/** @webhook-no-signature: <reason>  *\/` above the export.
 *
 * Detection limitation (v1): only matches `ImportSpecifier` named imports for
 * SDK verifiers (`import { verifyWebhookSignature } from ...`). Default or
 * namespace imports are NOT detected; if the SDK exposes the verifier those
 * ways, use the JSDoc opt-out and document the reason.
 */
import type { Rule } from 'eslint';
import type * as ESTree from 'estree';

const FILENAME_PATTERNS = [/\/webhooks?Parse\.ts$/i, /\/parseWebhooks?\.ts$/i, /\/webhookParse\.ts$/i];

const SDK_PACKAGES = new Set<string>(['@unito/integration-sdk', '@unito/integration-api']);

const PARSE_HANDLER_TYPE = 'ParseWebhooksHandler';
const ACK_HANDLER_TYPE = 'AcknowledgeWebhooksHandler';

const PARSE_EXPORT_NAME_RE = /^parseWebhooks?$/;

const CRYPTO_MODULES = new Set<string>(['crypto', 'node:crypto']);

const SDK_VERIFIER_NAMES = new Set<string>([
  'verifyWebhookSignature',
  'verifySignature',
  'verifyWebhook',
  'WebhookVerifier',
]);

const OPT_OUT_RE = /@webhook-no-signature:\s*\S+/;

function filenameInScope(filename: string): boolean {
  return FILENAME_PATTERNS.some(re => re.test(filename));
}

function isCryptoImport(node: ESTree.Node): boolean {
  if (node.type !== 'ImportDeclaration') {
    return false;
  }
  return typeof node.source.value === 'string' && CRYPTO_MODULES.has(node.source.value);
}

function isSdkVerifierImport(node: ESTree.Node): boolean {
  if (node.type !== 'ImportDeclaration') {
    return false;
  }
  if (typeof node.source.value !== 'string') {
    return false;
  }
  if (!SDK_PACKAGES.has(node.source.value)) {
    return false;
  }
  return node.specifiers.some(spec => {
    if (spec.type !== 'ImportSpecifier') {
      return false;
    }
    const importedName = spec.imported.type === 'Identifier' ? spec.imported.name : null;
    return importedName !== null && SDK_VERIFIER_NAMES.has(importedName);
  });
}

function importsParseHandlerType(node: ESTree.Node): boolean {
  if (node.type !== 'ImportDeclaration') {
    return false;
  }
  if (typeof node.source.value !== 'string') {
    return false;
  }
  if (!SDK_PACKAGES.has(node.source.value)) {
    return false;
  }
  return node.specifiers.some(spec => {
    if (spec.type !== 'ImportSpecifier') {
      return false;
    }
    return spec.imported.type === 'Identifier' && spec.imported.name === PARSE_HANDLER_TYPE;
  });
}

interface TypeAnnotationContainer {
  typeAnnotation?: {
    typeAnnotation?: {
      type: string;
      typeName?: { type: string; name: string };
    };
  };
}

function getTypeAnnotationName(typeAnnotation: TypeAnnotationContainer['typeAnnotation'] | undefined): string | null {
  if (!typeAnnotation) {
    return null;
  }
  const inner = typeAnnotation.typeAnnotation;
  if (!inner) {
    return null;
  }
  if (inner.type === 'TSTypeReference' && inner.typeName && inner.typeName.type === 'Identifier') {
    return inner.typeName.name;
  }
  return null;
}

interface ParseExportInfo {
  name: string;
}

function findParseExportFromDeclaration(decl: ESTree.VariableDeclaration | null | undefined): ParseExportInfo | null {
  if (!decl || decl.type !== 'VariableDeclaration') {
    return null;
  }
  for (const declarator of decl.declarations) {
    if (!declarator.id || declarator.id.type !== 'Identifier') {
      continue;
    }
    const name = declarator.id.name;
    const idAsContainer = declarator.id as unknown as TypeAnnotationContainer;
    const typeName = getTypeAnnotationName(idAsContainer.typeAnnotation);

    if (typeName === ACK_HANDLER_TYPE) {
      continue;
    }
    if (typeName === PARSE_HANDLER_TYPE || PARSE_EXPORT_NAME_RE.test(name)) {
      return { name };
    }
  }
  return null;
}

function findParseExport(node: ESTree.Node, programBody: readonly ESTree.Node[]): ParseExportInfo | null {
  if (node.type !== 'ExportNamedDeclaration') {
    return null;
  }

  if (node.declaration) {
    return findParseExportFromDeclaration(node.declaration as ESTree.VariableDeclaration);
  }

  if (!node.specifiers || node.specifiers.length === 0) {
    return null;
  }

  for (const spec of node.specifiers) {
    if (spec.type !== 'ExportSpecifier') {
      continue;
    }
    const exportedName = spec.exported.type === 'Identifier' ? spec.exported.name : null;
    const localName = spec.local.type === 'Identifier' ? spec.local.name : null;
    if (!exportedName || !localName) {
      continue;
    }

    let resolvedTypeName: string | null = null;
    for (const stmt of programBody) {
      if (stmt.type !== 'VariableDeclaration') {
        continue;
      }
      for (const d of stmt.declarations) {
        if (d.id && d.id.type === 'Identifier' && d.id.name === localName) {
          const idAsContainer = d.id as unknown as TypeAnnotationContainer;
          resolvedTypeName = getTypeAnnotationName(idAsContainer.typeAnnotation);
        }
      }
    }

    if (resolvedTypeName === ACK_HANDLER_TYPE) {
      continue;
    }
    if (
      resolvedTypeName === PARSE_HANDLER_TYPE ||
      PARSE_EXPORT_NAME_RE.test(exportedName) ||
      PARSE_EXPORT_NAME_RE.test(localName)
    ) {
      return { name: exportedName };
    }
  }

  return null;
}

function hasOptOutComment(sourceCode: Rule.RuleContext['sourceCode'], node: ESTree.Node): boolean {
  const comments = sourceCode.getCommentsBefore(node);
  return comments.some(c => OPT_OUT_RE.test(c.value));
}

const rule: Rule.RuleModule = {
  meta: {
    type: 'problem',
    docs: {
      description:
        'Require webhook parse handlers to import a signature-verification helper (crypto.createHmac/timingSafeEqual or a known SDK verifier), or carry a `@webhook-no-signature: <reason>` JSDoc opt-out.',
      recommended: true,
    },
    schema: [],
    messages: {
      missingVerification:
        'Webhook parse handler `{{name}}` has no signature-verification import. Webhook payloads are provider-controlled untrusted strings — verify the provider signature before parsing. Import `crypto.createHmac` + `timingSafeEqual` (or a known SDK verifier) and check the signature against `ctx.body.headers`. If the provider genuinely does not sign webhooks, document it with a JSDoc above the export: `/** @webhook-no-signature: <reason> */`.',
    },
  },

  create(context) {
    const filename = context.filename ?? '';
    const sourceCode = context.sourceCode;

    let inScopeByImport = false;
    let hasVerifierImport = false;

    return {
      ImportDeclaration(node) {
        if (isCryptoImport(node) || isSdkVerifierImport(node)) {
          hasVerifierImport = true;
        }
        if (importsParseHandlerType(node)) {
          inScopeByImport = true;
        }
      },

      'Program:exit'(programNode: ESTree.Program) {
        const program = programNode;
        const filenameMatch = filenameInScope(filename);
        const inScope = filenameMatch || inScopeByImport;
        if (!inScope) {
          return;
        }
        if (hasVerifierImport) {
          return;
        }

        let flagged = false;
        for (const stmt of program.body) {
          const found = findParseExport(stmt as ESTree.Node, program.body as readonly ESTree.Node[]);
          if (!found) {
            continue;
          }
          if (hasOptOutComment(sourceCode, stmt as ESTree.Node)) {
            flagged = true;
            continue;
          }
          flagged = true;
          context.report({
            node: stmt as ESTree.Node,
            messageId: 'missingVerification',
            data: { name: found.name },
          });
        }

        if (!flagged && filenameMatch) {
          const hasAnyExport = program.body.some(
            stmt => stmt.type === 'ExportNamedDeclaration' || stmt.type === 'ExportDefaultDeclaration',
          );
          if (!hasAnyExport) {
            return;
          }
          const firstStmt = program.body[0];
          if (!firstStmt) {
            return;
          }
          if (hasOptOutComment(sourceCode, firstStmt as ESTree.Node)) {
            return;
          }
          context.report({
            node: firstStmt as ESTree.Node,
            messageId: 'missingVerification',
            data: { name: '<webhook-parse-route>' },
          });
        }
      },
    };
  },
};

export default rule;
