import { readdirSync } from 'node:fs';
import { join, parse } from 'node:path';

const VALUE_OBJECTS_DIR = join(import.meta.dirname, '..', 'src', 'value-objects');

function discoverValueObjects() {
  const files = readdirSync(VALUE_OBJECTS_DIR).filter((f) => f.endsWith('.ts') && f !== 'index.ts');
  return files.map((f) => parse(f).name);
}

const VALUE_OBJECTS = discoverValueObjects();

function buildPropertyMap() {
  const map = {};
  for (const name of VALUE_OBJECTS) {
    const propertyName = name.charAt(0).toLowerCase() + name.slice(1);
    map[propertyName] = name;
  }
  return map;
}

const PROPERTY_MAP = buildPropertyMap();

/** Map ESLint AST type names to the primitive keyword string. */
const PRIMITIVE_TYPE_MAP = {
  TSNumberKeyword: 'number',
  TSStringKeyword: 'string',
  TSBooleanKeyword: 'boolean',
};

function isPrimitiveType(typeNode) {
  if (!typeNode) return false;
  const keyword = PRIMITIVE_TYPE_MAP[typeNode.type];
  return keyword !== undefined;
}

function getPrimitiveKeyword(typeNode) {
  if (!typeNode) return null;
  return PRIMITIVE_TYPE_MAP[typeNode.type] || null;
}

function getParamName(param) {
  if (param.type === 'TSParameterProperty') {
    return getParamName(param.parameter);
  }
  if (param.type === 'Identifier') {
    return param.name;
  }
  return null;
}

export default {
  meta: {
    type: 'suggestion',
    docs: {
      description:
        'Warn when primitive types are used in class properties or constructor parameters that should be value objects',
      recommended: false,
    },
    schema: [
      {
        type: 'object',
        properties: {
          includeConstructorParams: { type: 'boolean', default: true },
          includeProperties: { type: 'boolean', default: true },
        },
        additionalProperties: false,
      },
    ],
    messages: {
      primitiveProperty:
        'Use value object "{{valueObject}}" instead of primitive "{{primitive}}" for property "{{propertyName}}". Available: {{available}}',
      primitiveParam:
        'Use value object "{{valueObject}}" instead of primitive "{{primitive}}" for parameter "{{paramName}}". Available: {{available}}',
    },
  },

  create(context) {
    const options = context.options[0] || {};
    const includeConstructorParams = options.includeConstructorParams !== false;
    const includeProperties = options.includeProperties !== false;

    function reportPrimitive(node, propName, primitiveType, messageId) {
      const valueObject = PROPERTY_MAP[propName];
      if (valueObject) {
        context.report({
          node,
          messageId,
          data: {
            valueObject,
            primitive: primitiveType,
            propertyName: propName,
            paramName: propName,
            available: VALUE_OBJECTS.join(', '),
          },
        });
      }
    }

    return {
      ClassProperty(node) {
        if (!includeProperties) return;
        if (!node.typeAnnotation) return;

        const typeNode = node.typeAnnotation.typeAnnotation;
        if (!isPrimitiveType(typeNode)) return;

        reportPrimitive(node, node.key.name, getPrimitiveKeyword(typeNode), 'primitiveProperty');
      },

      PropertyDefinition(node) {
        if (!includeProperties) return;
        if (!node.typeAnnotation) return;

        const typeNode = node.typeAnnotation.typeAnnotation;
        if (!isPrimitiveType(typeNode)) return;

        reportPrimitive(node, node.key.name, getPrimitiveKeyword(typeNode), 'primitiveProperty');
      },

      MethodDefinition(node) {
        if (!includeConstructorParams) return;
        if (node.key.name !== 'constructor') return;
        if (!node.value.params) return;

        for (const param of node.value.params) {
          let paramName = null;
          let typeNode = null;

          if (param.type === 'TSParameterProperty') {
            paramName = getParamName(param.parameter);
            typeNode = param.parameter.typeAnnotation?.typeAnnotation;
          } else if (param.type === 'Identifier') {
            paramName = param.name;
            typeNode = param.typeAnnotation?.typeAnnotation;
          }

          if (!paramName || !typeNode || !isPrimitiveType(typeNode)) continue;

          reportPrimitive(param, paramName, getPrimitiveKeyword(typeNode), 'primitiveParam');
        }
      },
    };
  },
};
