/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

/**
 * Entity facet checker
 */

import type {
  IDSEntityFacet,
  IDSConstraint,
  IFCDataAccessor,
} from '../types.js';
import type { FacetCheckResult } from './index.js';
import { matchConstraint, formatConstraint } from '../constraints/index.js';

/** IFC entity/predefined type comparisons are case-insensitive per IDS spec */
const IFC_CASE_INSENSITIVE = { caseInsensitive: true } as const;

/**
 * Check if an entity matches an entity facet
 */
export function checkEntityFacet(
  facet: IDSEntityFacet,
  expressId: number,
  accessor: IFCDataAccessor
): FacetCheckResult {
  const entityType = accessor.getEntityType(expressId);

  if (!entityType) {
    return {
      passed: false,
      actualValue: undefined,
      expectedValue: formatConstraint(facet.name),
      failure: {
        type: 'ENTITY_TYPE_MISMATCH',
        field: 'entityType',
        actual: 'unknown',
        expected: formatConstraint(facet.name),
      },
    };
  }

  // Per IDS 1.0 spec, entity-name simpleValue literals MUST be
  // uppercase (`IFCWALL`, not `IfcWall`). Reject malformed authoring
  // outright before attempting the case-insensitive comparison —
  // otherwise mixed-case literals would silently match.
  if (
    facet.name.type === 'simpleValue' &&
    facet.name.value !== facet.name.value.toUpperCase()
  ) {
    return {
      passed: false,
      actualValue: entityType,
      expectedValue: formatConstraint(facet.name),
      failure: {
        type: 'ENTITY_TYPE_MISMATCH',
        field: 'entityType',
        actual: entityType,
        expected: formatConstraint(facet.name),
      },
    };
  }

  // Check entity type (case-insensitive per IDS spec — IFC entity names are case-agnostic)
  if (!matchConstraint(facet.name, entityType, IFC_CASE_INSENSITIVE)) {
    return {
      passed: false,
      actualValue: entityType,
      expectedValue: formatConstraint(facet.name),
      failure: {
        type: 'ENTITY_TYPE_MISMATCH',
        field: 'entityType',
        actual: entityType,
        expected: formatConstraint(facet.name),
      },
    };
  }

  // Check predefined type if specified
  if (facet.predefinedType) {
    // Per IDS spec, predefined-type matching has two distinct paths:
    //   1. Compare against the raw IFC `PredefinedType` enum token
    //      (BEAM, USERDEFINED, NOTDEFINED, …) — case-insensitive.
    //   2. When the raw token is `USERDEFINED`, fall back to the
    //      user-defined name (`ObjectType`/`ElementType`/`ProcessType`)
    //      — case-sensitive.
    // The order matters: a fixture asking for `USERDEFINED` literally
    // must match an entity whose enum is `USERDEFINED` regardless of
    // its accompanying user-defined name.
    const rawType = accessor.getPredefinedTypeRaw?.(expressId);
    const userDefinedType = accessor.getObjectType(expressId);

    if (!rawType && !userDefinedType) {
      return {
        passed: false,
        actualValue: entityType,
        expectedValue: `${formatConstraint(facet.name)} with predefinedType ${formatConstraint(facet.predefinedType)}`,
        failure: {
          type: 'PREDEFINED_TYPE_MISSING',
          field: 'predefinedType',
          expected: formatConstraint(facet.predefinedType),
        },
      };
    }

    let matched = false;
    // Predefined-type enum tokens (BEAM, USERDEFINED, …) MUST be
    // uppercase per the IFC schema, and the IDS literal MUST match
    // exactly. Case-sensitive comparison is the spec.
    if (rawType && matchConstraint(facet.predefinedType, rawType)) {
      matched = true;
    } else if (
      rawType === 'USERDEFINED' &&
      userDefinedType &&
      userDefinedType !== rawType &&
      matchConstraint(facet.predefinedType, userDefinedType)
    ) {
      // Case-sensitive comparison for user-defined names.
      matched = true;
    } else if (
      !rawType &&
      userDefinedType &&
      matchConstraint(facet.predefinedType, userDefinedType)
    ) {
      // No raw enum reported (legacy accessor) — case-sensitive match
      // against the substituted form.
      matched = true;
    }

    if (!matched) {
      const display = userDefinedType || rawType || '(none)';
      return {
        passed: false,
        actualValue: `${entityType}[${display}]`,
        expectedValue: `${formatConstraint(facet.name)} with predefinedType ${formatConstraint(facet.predefinedType)}`,
        failure: {
          type: 'PREDEFINED_TYPE_MISMATCH',
          field: 'predefinedType',
          actual: display,
          expected: formatConstraint(facet.predefinedType),
        },
      };
    }
  }

  return {
    passed: true,
    actualValue: facet.predefinedType
      ? `${entityType}[${accessor.getObjectType(expressId) || ''}]`
      : entityType,
    expectedValue: formatConstraint(facet.name),
  };
}

/**
 * Diagnostics-free verdict for an entity facet — the exact `passed`
 * boolean `checkEntityFacet` would compute, without allocating failure
 * objects or display strings. Applicability filtering runs this against
 * every candidate entity for every specification and reads ONLY the
 * boolean, so the diagnostic work was pure waste (~86% of validation
 * time on code-list IDS packs).
 *
 * Any semantic change to `checkEntityFacet` MUST be mirrored here; the
 * differential test in validation-scale.test.ts pins the equivalence.
 */
export function entityFacetPasses(
  facet: IDSEntityFacet,
  expressId: number,
  accessor: IFCDataAccessor
): boolean {
  const entityType = accessor.getEntityType(expressId);
  if (!entityType) return false;

  // Mixed-case simpleValue literals are malformed authoring — rejected
  // outright (mirrors checkEntityFacet).
  if (
    facet.name.type === 'simpleValue' &&
    facet.name.value !== facet.name.value.toUpperCase()
  ) {
    return false;
  }

  if (!matchConstraint(facet.name, entityType, IFC_CASE_INSENSITIVE)) {
    return false;
  }

  if (facet.predefinedType) {
    const rawType = accessor.getPredefinedTypeRaw?.(expressId);
    const userDefinedType = accessor.getObjectType(expressId);

    if (!rawType && !userDefinedType) return false;

    if (rawType && matchConstraint(facet.predefinedType, rawType)) {
      return true;
    }
    if (
      rawType === 'USERDEFINED' &&
      userDefinedType &&
      userDefinedType !== rawType &&
      matchConstraint(facet.predefinedType, userDefinedType)
    ) {
      return true;
    }
    if (
      !rawType &&
      userDefinedType &&
      matchConstraint(facet.predefinedType, userDefinedType)
    ) {
      return true;
    }
    return false;
  }

  return true;
}

/**
 * Get candidate entity IDs that might match an entity facet (broadphase filter)
 */
export function filterByEntityFacet(
  facet: IDSEntityFacet,
  accessor: IFCDataAccessor
): number[] | undefined {
  const constraint = facet.name;

  // For simple values, we can efficiently filter by type
  if (constraint.type === 'simpleValue') {
    return accessor.getEntitiesByType(constraint.value);
  }

  // For enumerations, collect entities of all specified types
  if (constraint.type === 'enumeration') {
    const ids: number[] = [];
    for (const value of constraint.values) {
      ids.push(...accessor.getEntitiesByType(value));
    }
    return ids;
  }

  // For patterns, we need to check all entity types
  // Return undefined to indicate full scan needed
  return undefined;
}

/**
 * Get all entity types that could match a constraint
 */
export function getMatchingEntityTypes(
  constraint: IDSConstraint,
  allTypes: string[]
): string[] {
  switch (constraint.type) {
    case 'simpleValue':
      return allTypes.filter(
        (t) => t.toUpperCase() === constraint.value.toUpperCase()
      );
    case 'enumeration':
      return allTypes.filter((t) =>
        constraint.values.some(
          (v) => v.toUpperCase() === t.toUpperCase()
        )
      );
    case 'pattern':
      try {
        const regex = new RegExp(`^${constraint.pattern}$`, 'i');
        return allTypes.filter((t) => regex.test(t));
      } catch {
        return [];
      }
    default:
      return allTypes;
  }
}
