/**
 * ESLint Rule: Mutation Must Map Errors
 *
 * Enforces the architecture documented in `lib/contracts/mutations/Mutation.ts`:
 * - Mutations call Services.
 * - Service returns Result<..., DomainError>.
 * - Mutation maps DomainError -> MutationError via mapToMutationError.
 * - Mutation returns Result<..., MutationError>.
 *
 * Strict enforcement (scoped to service results):
 * - Disallow returning a Service Result directly.
 *   - `return result;` where `result` came from `await service.*(...)`
 *   - `return service.someMethod(...)`
 *   - `return await service.someMethod(...)`
 * - If a mutation does `if (result.isErr()) return Result.err(...)`,
 *   it must be `Result.err(mapToMutationError(result.getError()))`.
 */

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

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

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

    const resultVarsFromServiceCall = new Set();
    const serviceVarNames = new Set();
    const mapToMutationErrorNames = new Set(['mapToMutationError']);

    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 isMapToMutationErrorCall(node) {
      return (
        node &&
        node.type === 'CallExpression' &&
        node.callee &&
        node.callee.type === 'Identifier' &&
        mapToMutationErrorNames.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 isServiceObjectName(name) {
      return typeof name === 'string' && (name.endsWith('Service') || serviceVarNames.has(name));
    }

    function isServiceMemberCallExpression(callExpression) {
      if (!callExpression || callExpression.type !== 'CallExpression') return false;
      const callee = callExpression.callee;
      if (!callee || callee.type !== 'MemberExpression' || callee.computed) return false;
      if (!callee.object || callee.object.type !== 'Identifier') return false;
      return isServiceObjectName(callee.object.name);
    }

    function isAwaitedServiceCall(init) {
      if (!init || init.type !== 'AwaitExpression') return false;
      return isServiceMemberCallExpression(init.argument);
    }

    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 isMutationErrorImport =
          importPath.includes('/lib/contracts/mutations/MutationError') ||
          importPath.endsWith('lib/contracts/mutations/MutationError') ||
          importPath.endsWith('contracts/mutations/MutationError') ||
          importPath.endsWith('contracts/mutations/MutationError.ts');

        if (!isMutationErrorImport) return;

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

      VariableDeclarator(node) {
        if (!node.id || node.id.type !== 'Identifier') return;

        // const service = new SomeService();
        if (node.init && node.init.type === 'NewExpression' && node.init.callee && node.init.callee.type === 'Identifier') {
          if (node.init.callee.name.endsWith('Service')) {
            serviceVarNames.add(node.id.name);
          }
        }

        // const result = await service.method(...)
        if (!isAwaitedServiceCall(node.init)) return;
        resultVarsFromServiceCall.add(node.id.name);
      },

      ReturnStatement(node) {
        if (!node.argument) return;

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

        // return service.method(...)
        if (node.argument.type === 'CallExpression') {
          if (isServiceMemberCallExpression(node.argument)) {
            context.report({ node, messageId: 'mustNotReturnServiceResult' });
          }
          return;
        }

        // return await service.method(...)
        if (node.argument.type === 'AwaitExpression') {
          if (isServiceMemberCallExpression(node.argument.argument)) {
            context.report({ node, messageId: 'mustNotReturnServiceResult' });
          }
        }
      },

      IfStatement(node) {
        // if (result.isErr()) { return Result.err(...) }
        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 (!isMapToMutationErrorCall(errArg)) {
            context.report({ node: returnNode, messageId: 'mustMapMutationError' });
            return;
          }

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