/**
 * ESLint Rule: PageQuery Must Map Errors
 *
 * Enforces the architecture documented in `lib/contracts/page-queries/PageQuery.ts`:
 * - PageQueries call Services.
 * - Service returns Result<..., DomainError>.
 * - PageQuery maps DomainError -> PresentationError via mapToPresentationError.
 * - PageQuery returns Result<..., PresentationError>.
 *
 * Strict enforcement (scoped to service-result error branches):
 * - If a page query does `const result = await someService.*(...)` and later does
 *   `if (result.isErr()) return Result.err(...)`, then the `Result.err(...)`
 *   must be `Result.err(mapToPresentationError(result.getError()))`.
 * - PageQueries must not `return result;` for a service result variable.
 */

module.exports = {
  meta: {
    type: 'problem',
    docs: {
      description: 'Require PageQueries to map service errors via mapToPresentationError',
      category: 'Architecture',
      recommended: true,
    },
    schema: [],
    messages: {
      mustMapPresentationError:
        'PageQueries must map service errors via mapToPresentationError(result.getError()) before returning Result.err(...) - see apps/website/lib/contracts/page-queries/PageQuery.ts',
      mustNotReturnServiceResult:
        'PageQueries must not return a Service Result directly; map errors and return a Presentation-layer Result instead - see apps/website/lib/contracts/page-queries/PageQuery.ts',
    },
  },

  create(context) {
    const filename = context.getFilename();

    if (!filename.includes('/lib/page-queries/') || !filename.endsWith('.ts')) {
      return {};
    }

    const resultVarsFromServiceCall = new Set();
    const mapToPresentationErrorNames = new Set(['mapToPresentationError']);

    function isIdentifier(node, name) {
      return node && node.type === 'Identifier' && node.name === name;
    }

    function isResultErrCall(node) {
      return (
        node &&
        node.type === 'CallExpression' &&
        node.callee &&
        node.callee.type === 'MemberExpression' &&
        !node.callee.computed &&
        node.callee.object &&
        node.callee.object.type === 'Identifier' &&
        node.callee.object.name === 'Result' &&
        node.callee.property &&
        node.callee.property.type === 'Identifier' &&
        node.callee.property.name === 'err'
      );
    }

    function isMapToPresentationErrorCall(node) {
      return (
        node &&
        node.type === 'CallExpression' &&
        node.callee &&
        node.callee.type === 'Identifier' &&
        mapToPresentationErrorNames.has(node.callee.name)
      );
    }

    function isGetErrorCall(node, resultVarName) {
      return (
        node &&
        node.type === 'CallExpression' &&
        node.callee &&
        node.callee.type === 'MemberExpression' &&
        !node.callee.computed &&
        node.callee.object &&
        isIdentifier(node.callee.object, resultVarName) &&
        node.callee.property &&
        node.callee.property.type === 'Identifier' &&
        node.callee.property.name === 'getError'
      );
    }

    function isAwaitedServiceCall(init) {
      if (!init || init.type !== 'AwaitExpression') return false;
      const call = init.argument;
      if (!call || call.type !== 'CallExpression') return false;
      const callee = call.callee;

      // `await service.someMethod(...)`
      if (callee.type === 'MemberExpression' && callee.object && callee.object.type === 'Identifier') {
        return callee.object.name.endsWith('Service');
      }

      return false;
    }

    function collectReturnStatements(statementNode, callback) {
      if (!statementNode) return;

      if (statementNode.type !== 'BlockStatement') {
        if (statementNode.type === 'ReturnStatement') callback(statementNode);
        return;
      }

      for (const s of statementNode.body || []) {
        if (!s) continue;
        if (s.type === 'ReturnStatement') callback(s);
      }
    }

    return {
      ImportDeclaration(node) {
        const importPath = node.source && node.source.value;
        if (typeof importPath !== 'string') return;

        const isPresentationErrorImport =
          importPath.includes('/lib/contracts/page-queries/PresentationError') ||
          importPath.endsWith('lib/contracts/page-queries/PresentationError') ||
          importPath.endsWith('contracts/page-queries/PresentationError') ||
          importPath.endsWith('contracts/page-queries/PresentationError.ts');

        if (!isPresentationErrorImport) return;

        for (const spec of node.specifiers || []) {
          // import { mapToPresentationError as X } from '...'
          if (spec.type === 'ImportSpecifier' && spec.imported && spec.imported.type === 'Identifier') {
            if (spec.imported.name === 'mapToPresentationError') {
              mapToPresentationErrorNames.add(spec.local.name);
            }
          }
        }
      },

      VariableDeclarator(node) {
        if (!node.id || node.id.type !== 'Identifier') return;
        if (!isAwaitedServiceCall(node.init)) return;
        resultVarsFromServiceCall.add(node.id.name);
      },

      ReturnStatement(node) {
        if (node.argument && node.argument.type === 'Identifier') {
          if (resultVarsFromServiceCall.has(node.argument.name)) {
            context.report({ node, messageId: 'mustNotReturnServiceResult' });
          }
        }
      },

      IfStatement(node) {
        const test = node.test;
        if (!test || test.type !== 'CallExpression') return;
        const testCallee = test.callee;
        if (
          !testCallee ||
          testCallee.type !== 'MemberExpression' ||
          testCallee.computed ||
          !testCallee.object ||
          testCallee.object.type !== 'Identifier' ||
          !testCallee.property ||
          testCallee.property.type !== 'Identifier' ||
          testCallee.property.name !== 'isErr'
        ) {
          return;
        }

        const resultVarName = testCallee.object.name;
        if (!resultVarsFromServiceCall.has(resultVarName)) return;

        collectReturnStatements(node.consequent, (returnNode) => {
          const arg = returnNode.argument;
          if (!isResultErrCall(arg)) return;

          const errArg = arg.arguments && arg.arguments[0];
          if (!isMapToPresentationErrorCall(errArg)) {
            context.report({ node: returnNode, messageId: 'mustMapPresentationError' });
            return;
          }

          const mappedFrom = errArg.arguments && errArg.arguments[0];
          if (!isGetErrorCall(mappedFrom, resultVarName)) {
            context.report({ node: returnNode, messageId: 'mustMapPresentationError' });
          }
        });
      },
    };
  },
};
