import { Graph, Id as Grc20Id } from '@graphprotocol/grc-20';
import { Data, Array as EffectArray, Schema as EffectSchema, Option, pipe } from 'effect';
import { namesAreUnique, toCamelCase, toPascalCase } from './Utils.js';
/**
 * @since 0.2.0
 */
export function isDataTypeRelation(val) {
  return /^Relation\((.+)\)$/.test(val);
}
/**
 * @since 0.2.0
 */
export const SchemaDataTypeRelation = /*#__PURE__*/EffectSchema.NonEmptyTrimmedString.pipe(/*#__PURE__*/EffectSchema.filter(val => isDataTypeRelation(val)));
/**
 * @since 0.2.0
 */
export const SchemaDataTypePrimitive = /*#__PURE__*/EffectSchema.Literal('String', 'Number', 'Boolean', 'Date', 'Point');
/**
 * @since 0.4.0
 */
export function isDataTypePrimitive(val) {
  return ['String', 'Number', 'Boolean', 'Date', 'Point'].includes(val);
}
/**
 * @since 0.2.0
 */
export const SchemaDataType = /*#__PURE__*/EffectSchema.Union(SchemaDataTypePrimitive, SchemaDataTypeRelation);
/**
 * @since 0.4.0
 */
export function isDataType(val) {
  return isDataTypePrimitive(val) || isDataTypeRelation(val);
}
/**
 * @since 0.4.0
 */
export function getDataType(val) {
  const dataType = isDataTypePrimitive(val) || isDataTypeRelation(val);
  if (dataType) {
    return val;
  }
  throw new Error(`Passed dataType ${val} is not supported`);
}
const BaseSchemaTypeProperty = /*#__PURE__*/EffectSchema.Struct({
  name: EffectSchema.NonEmptyTrimmedString,
  knowledgeGraphId: /*#__PURE__*/EffectSchema.NullOr(EffectSchema.UUID),
  /**
   * @since 0.4.0
   */
  optional: /*#__PURE__*/EffectSchema.optional(/*#__PURE__*/EffectSchema.NullishOr(EffectSchema.Boolean))
});
/**
 * @since 0.2.0
 */
export const SchemaTypePropertyRelation = /*#__PURE__*/EffectSchema.Struct({
  ...BaseSchemaTypeProperty.fields,
  dataType: SchemaDataTypeRelation,
  relationType: /*#__PURE__*/EffectSchema.NonEmptyTrimmedString.annotations({
    identifier: 'SchemaTypePropertyRelation.relationType',
    description: 'name of the type within the schema that this property is related to',
    examples: ['Account']
  })
});
/**
 * @since 0.2.0
 */
export const SchemaTypePropertyPrimitive = /*#__PURE__*/EffectSchema.Struct({
  ...BaseSchemaTypeProperty.fields,
  dataType: SchemaDataTypePrimitive
});
/**
 * @since 0.2.0
 */
export function propertyIsRelation(property) {
  return isDataTypeRelation(property.dataType);
}
/**
 * @since 0.2.0
 */
export const SchemaType = /*#__PURE__*/EffectSchema.Struct({
  name: EffectSchema.NonEmptyTrimmedString,
  knowledgeGraphId: /*#__PURE__*/EffectSchema.NullOr(EffectSchema.UUID),
  properties: /*#__PURE__*/EffectSchema.Array(EffectSchema.Union(SchemaTypePropertyPrimitive, SchemaTypePropertyRelation)).pipe(/*#__PURE__*/EffectSchema.minItems(1), /*#__PURE__*/EffectSchema.filter(namesAreUnique, {
    identifier: 'DuplicatePropertyNames',
    jsonSchema: {},
    description: 'The property.name must be unique across all properties in the type'
  }))
});
/**
 * Represents the user-built schema object to generate a `Mappings` definition for
 *
 * @since 0.2.0
 */
export const Schema = /*#__PURE__*/EffectSchema.Struct({
  types: EffectSchema.Array(SchemaType).pipe(EffectSchema.minItems(1), EffectSchema.filter(namesAreUnique, {
    identifier: 'DuplicateTypeNames',
    jsonSchema: {},
    description: 'The type.name must be unique across all types in the schema'
  }), EffectSchema.filter(allRelationPropertyTypesExist, {
    identifier: 'AllRelationTypesExist',
    jsonSchema: {},
    description: 'Each type property of dataType RELATION must have a type of the same name in the schema'
  }))
}).annotations({
  identifier: 'typesync/Schema',
  title: 'TypeSync app Schema',
  description: 'An array of types in the schema defined by the user to generate a Mapping object for',
  examples: [{
    types: [{
      name: 'Account',
      knowledgeGraphId: null,
      properties: [{
        name: 'username',
        optional: null,
        knowledgeGraphId: null,
        dataType: 'String'
      }]
    }]
  }, {
    types: [{
      name: 'Account',
      knowledgeGraphId: 'a5fd07b1-120f-46c6-b46f-387ef98396a6',
      properties: [{
        name: 'name',
        optional: true,
        knowledgeGraphId: 'a126ca53-0c8e-48d5-b888-82c734c38935',
        dataType: 'String'
      }]
    }]
  }]
});
/**
 * @since 0.2.0
 */
export const SchemaKnownDecoder = /*#__PURE__*/EffectSchema.decodeSync(Schema);
/**
 * @since 0.2.0
 */
export const SchemaUnknownDecoder = /*#__PURE__*/EffectSchema.decodeUnknownSync(Schema);
/**
 * Iterate through all properties in all types in the schema of `dataType` === `Relation(${string})`
 * and validate that the schema.types have a type for the existing relation
 *
 * @example <caption>All types exist</caption>
 * ```ts
 * import { allRelationPropertyTypesExist, type Mapping } from '@graphprotocol/hypergraph/mapping'
 *
 * const types: Mapping['types'] = [
 *   {
 *     name: "Account",
 *     knowledgeGraphId: null,
 *     properties: [
 *       {
 *         name: "username",
 *         dataType: "String",
 *         knowledgeGraphId: null
 *       }
 *     ]
 *   },
 *   {
 *     name: "Event",
 *     knowledgeGraphId: null,
 *     properties: [
 *       {
 *         name: "speaker",
 *         dataType: "Relation(Account)"
 *         relationType: "Account",
 *         knowledgeGraphId: null,
 *       }
 *     ]
 *   }
 * ]
 * expect(allRelationPropertyTypesExist(types)).toEqual(true)
 * ```
 *
 * @example <caption>Account type is missing</caption>
 * ```ts
 * import { allRelationPropertyTypesExist, type Mapping } from '@graphprotocol/hypergraph/mapping'
 *
 * const types: Mapping['types'] = [
 *   {
 *     name: "Event",
 *     knowledgeGraphId: null,
 *     properties: [
 *       {
 *         name: "speaker",
 *         dataType: "Relation(Account)",
 *         relationType: "Account",
 *         knowledgeGraphId: null,
 *       }
 *     ]
 *   }
 * ]
 * expect(allRelationPropertyTypesExist(types)).toEqual(false)
 * ```
 *
 * @since 0.2.0
 *
 * @param types the user-submitted schema types
 */
export function allRelationPropertyTypesExist(types) {
  const unqTypeNames = EffectArray.reduce(types, new Set(), (names, curr) => names.add(curr.name));
  return pipe(types, EffectArray.flatMap(curr => curr.properties), EffectArray.filter(prop => propertyIsRelation(prop)), EffectArray.every(prop => unqTypeNames.has(prop.relationType)));
}
// Helper function to build property map from PropertyIdMappings
function buildPropertyMap(properties) {
  return pipe(properties, EffectArray.reduce({}, (props, {
    propName,
    id
  }) => {
    props[toCamelCase(propName)] = id;
    return props;
  }));
}
// Helper function to build relation map from PropertyIdMappings
function buildRelationMap(relations) {
  return pipe(relations, EffectArray.reduce({}, (rels, {
    propName,
    id
  }) => {
    rels[toCamelCase(propName)] = id;
    return rels;
  }));
}
// Helper function to create a property and return the result
function createPropertyWithOps(property, typeIdMap) {
  if (property.knowledgeGraphId) {
    return {
      type: 'resolved',
      mapping: {
        propName: property.name,
        id: Grc20Id(property.knowledgeGraphId)
      },
      ops: []
    };
  }
  if (propertyIsRelation(property)) {
    const relationTypeId = typeIdMap.get(property.relationType);
    if (relationTypeId == null) {
      return {
        type: 'deferred',
        property
      };
    }
    const {
      id,
      ops
    } = Graph.createProperty({
      name: property.name,
      dataType: 'RELATION',
      relationValueTypes: [relationTypeId]
    });
    return {
      type: 'resolved',
      mapping: {
        propName: property.name,
        id
      },
      ops
    };
  }
  const {
    id,
    ops
  } = Graph.createProperty({
    name: property.name,
    dataType: mapSchemaDataTypeToGRC20PropDataType(property.dataType)
  });
  return {
    type: 'resolved',
    mapping: {
      propName: property.name,
      id
    },
    ops
  };
}
// Helper function to process a single type
function processType(type, typeIdMap) {
  const processedProperties = pipe(type.properties, EffectArray.map(prop => createPropertyWithOps(prop, typeIdMap)));
  const resolvedProperties = pipe(processedProperties, EffectArray.filterMap(p => p.type === 'resolved' ? Option.some(p) : Option.none()));
  const deferredProperties = pipe(processedProperties, EffectArray.filterMap(p => p.type === 'deferred' ? Option.some(p.property) : Option.none()));
  // Separate resolved properties into primitive properties and relations
  const primitiveProperties = pipe(resolvedProperties, EffectArray.filter(p => {
    const originalProp = type.properties.find(prop => prop.name === p.mapping.propName);
    return originalProp ? !propertyIsRelation(originalProp) : false;
  }), EffectArray.map(p => p.mapping));
  const relationProperties = pipe(resolvedProperties, EffectArray.filter(p => {
    const originalProp = type.properties.find(prop => prop.name === p.mapping.propName);
    return originalProp ? propertyIsRelation(originalProp) : false;
  }), EffectArray.map(p => p.mapping));
  const propertyOps = pipe(resolvedProperties, EffectArray.flatMap(p => p.ops));
  // If type exists in knowledge graph, return complete entry
  if (type.knowledgeGraphId) {
    const entry = {
      typeName: toPascalCase(type.name),
      typeIds: [Grc20Id(type.knowledgeGraphId)]
    };
    if (EffectArray.isNonEmptyArray(primitiveProperties)) {
      entry.properties = buildPropertyMap(primitiveProperties);
    }
    if (EffectArray.isNonEmptyArray(relationProperties)) {
      entry.relations = buildRelationMap(relationProperties);
    }
    return {
      type: 'complete',
      entry,
      ops: propertyOps
    };
  }
  // If there are deferred properties, defer type creation
  if (EffectArray.isNonEmptyArray(deferredProperties)) {
    return {
      type: 'deferred',
      schemaType: type,
      properties: primitiveProperties,
      relations: relationProperties
    };
  }
  // Create the type with all resolved properties (both primitive and relations)
  const allPropertyIds = [...primitiveProperties, ...relationProperties];
  const {
    id,
    ops: typeOps
  } = Graph.createType({
    name: type.name,
    properties: pipe(allPropertyIds, EffectArray.map(p => p.id))
  });
  typeIdMap.set(type.name, id);
  const entry = {
    typeName: toPascalCase(type.name),
    typeIds: [id]
  };
  if (EffectArray.isNonEmptyArray(primitiveProperties)) {
    entry.properties = buildPropertyMap(primitiveProperties);
  }
  if (EffectArray.isNonEmptyArray(relationProperties)) {
    entry.relations = buildRelationMap(relationProperties);
  }
  return {
    type: 'complete',
    entry,
    ops: [...propertyOps, ...typeOps]
  };
}
/**
 * Takes the user-submitted schema, validates it, and build the `Mapping` definition for the schema as well as the GRC-20 Ops needed to publish the schema/schema changes to the Knowledge Graph.
 *
 * @example
 * ```ts
 * import { Id } from "@graphprotocol/grc-20"
 * import { generateMapping } from "@graphprotocol/hypergraph"
 *
 * const schema: Schema = {
 *   types: [
 *     {
 *       name: "Account",
 *       knowledgeGraphId: "a5fd07b1-120f-46c6-b46f-387ef98396a6",
 *       properties: [
 *         {
 *           name: "username",
 *           dataType: "String",
 *           knowledgeGraphId: "994edcff-6996-4a77-9797-a13e5e3efad8"
 *         },
 *         {
 *           name: "createdAt",
 *           dataType: "Date",
 *           knowledgeGraphId: null
 *         }
 *       ]
 *     },
 *     {
 *       name: "Event",
 *       knowledgeGraphId: null,
 *       properties: [
 *         {
 *           name: "name",
 *           dataType: "String",
 *           knowledgeGraphId: "3808e060-fb4a-4d08-8069-35b8c8a1902b"
 *         },
 *         {
 *           name: "description",
 *           dataType: "String",
 *           knowledgeGraphId: null
 *         },
 *         {
 *           name: "speaker",
 *           dataType: "Relation(Account)",
 *           relationType: "Account",
 *           knowledgeGraphId: null
 *         }
 *       ]
 *     }
 *   ],
 * }
 * const [mapping, ops] = generateMapping(schema)
 *
 * expect(mapping).toEqual({
 *   Account: {
 *     typeIds: [Id("a5fd07b1-120f-46c6-b46f-387ef98396a6")], // comes from input schema
 *     properties: {
 *       username: Id("994edcff-6996-4a77-9797-a13e5e3efad8"), // comes from input schema
 *       createdAt: Id("8cd7d9ac-a878-4287-8000-e71e6f853117"), // generated from Graph.createProperty Op
 *     }
 *   },
 *   Event: {
 *     typeIds: [Id("20b3fe39-8e62-41a0-b9cb-92743fd760da")], // generated from Graph.createType Op
 *     properties: {
 *       name: Id("3808e060-fb4a-4d08-8069-35b8c8a1902b"), // comes from input schema
 *       description: Id("8fc4e17c-7581-4d6c-a712-943385afc7b5"), // generated from Graph.createProperty Op
 *     },
 *     relations: {
 *       speaker: Id("651ce59f-643b-4931-bf7a-5dc0ca0f5a47"), // generated from Graph.createProperty Op
 *     }
 *   }
 * })
 * expect(ops).toEqual([
 *   // Graph.createProperty Op for Account.createdAt property
 *   {
 *     type: "CREATE_PROPERTY",
 *     property: {
 *       id: Id("8cd7d9ac-a878-4287-8000-e71e6f853117"),
 *       dataType: "String"
 *     }
 *   },
 *   // Graph.createProperty Op for Event.description property
 *   {
 *     type: "CREATE_PROPERTY",
 *     property: {
 *       id: Id("8fc4e17c-7581-4d6c-a712-943385afc7b5"),
 *       dataType: "String"
 *     }
 *   },
 *   // Graph.createProperty Op for Event.speaker property
 *   {
 *     type: "CREATE_PROPERTY",
 *     property: {
 *       id: Id("651ce59f-643b-4931-bf7a-5dc0ca0f5a47"),
 *       dataType: "RELATION"
 *     }
 *   },
 *   // Graph.createType Op for Event type
 *   {
 *     type: "CREATE_PROPERTY",
 *     property: {
 *       id: Id("651ce59f-643b-4931-bf7a-5dc0ca0f5a47"),
 *       dataType: "RELATION"
 *     }
 *   },
 * ])
 * ```
 *
 * @since 0.2.0
 *
 * @param input user-built and submitted schema
 * @returns the generated [Mapping] definition from the submitted schema as well as the GRC-20 Ops required to publish the schema to the Knowledge Graph
 */
export function generateMapping(input) {
  // Validate the schema
  const schema = SchemaKnownDecoder(input);
  // Build initial type ID map
  const typeIdMap = pipe(schema.types, EffectArray.reduce(new Map(), (map, type) => map.set(type.name, type.knowledgeGraphId != null ? Grc20Id(type.knowledgeGraphId) : null)));
  // First pass: process all types
  const processedTypes = pipe(schema.types, EffectArray.map(type => processType(type, typeIdMap)));
  // Separate complete and deferred types
  const [deferredTypes, completeTypes] = pipe(processedTypes, EffectArray.partition(result => result.type === 'complete'));
  // Collect all operations from first pass
  const firstPassOps = pipe(completeTypes, EffectArray.flatMap(t => t.ops));
  // Second pass: resolve deferred relation properties and create deferred types
  const {
    entries: deferredEntries,
    ops: secondPassOps
  } = pipe(deferredTypes, EffectArray.reduce({
    entries: [],
    ops: []
  }, (acc, deferred) => {
    // Resolve all deferred relation properties for this type
    const resolvedRelations = pipe(deferred.schemaType.properties, EffectArray.filterMap(prop => {
      if (!propertyIsRelation(prop) || prop.knowledgeGraphId != null) {
        return Option.none();
      }
      const relationTypeId = typeIdMap.get(prop.relationType);
      if (relationTypeId == null) {
        throw new RelationValueTypeDoesNotExistError({
          message: `Failed to resolve type ID for relation type: ${prop.relationType}`,
          property: prop.name,
          relatedType: prop.relationType
        });
      }
      const {
        id,
        ops
      } = Graph.createProperty({
        name: prop.name,
        dataType: 'RELATION',
        relationValueTypes: [relationTypeId]
      });
      return Option.some({
        mapping: {
          propName: prop.name,
          id
        },
        ops
      });
    }));
    // Combine resolved relations with existing relations
    const allRelations = [...deferred.relations, ...pipe(resolvedRelations, EffectArray.map(r => r.mapping))];
    // Combine all property IDs for type creation
    const allPropertyIds = [...deferred.properties, ...allRelations];
    // Create the type with all properties
    const {
      id,
      ops: typeOps
    } = Graph.createType({
      name: deferred.schemaType.name,
      properties: pipe(allPropertyIds, EffectArray.map(p => p.id))
    });
    typeIdMap.set(deferred.schemaType.name, id);
    // Collect all operations
    const allOps = [...pipe(resolvedRelations, EffectArray.flatMap(r => r.ops)), ...typeOps];
    // Build the entry with properties and relations separated
    const entry = {
      typeName: toPascalCase(deferred.schemaType.name),
      typeIds: [id]
    };
    if (EffectArray.isNonEmptyArray(deferred.properties)) {
      entry.properties = buildPropertyMap(deferred.properties);
    }
    if (EffectArray.isNonEmptyArray(allRelations)) {
      entry.relations = buildRelationMap(allRelations);
    }
    return {
      entries: [...acc.entries, entry],
      ops: [...acc.ops, ...allOps]
    };
  }));
  // Combine all entries and build final mapping
  const allEntries = [...pipe(completeTypes, EffectArray.map(t => t.entry)), ...deferredEntries];
  const mapping = pipe(allEntries, EffectArray.reduce({}, (mapping, entry) => {
    const {
      typeName,
      ...rest
    } = entry;
    mapping[typeName] = rest;
    return mapping;
  }));
  return [mapping, [...firstPassOps, ...secondPassOps]];
}
export class RelationValueTypeDoesNotExistError extends /*#__PURE__*/Data.TaggedError('/typesync/errors/RelationValueTypeDoesNotExistError') {}
/**
 * @since 0.2.0
 *
 * @param dataType the dataType from the user-submitted schema
 * @returns the mapped to GRC-20 dataType for the GRC-20 ops
 */
export function mapSchemaDataTypeToGRC20PropDataType(dataType) {
  switch (true) {
    case dataType === 'Boolean':
      {
        return 'BOOLEAN';
      }
    case dataType === 'Date':
      {
        return 'TIME';
      }
    case dataType === 'Number':
      {
        return 'NUMBER';
      }
    case dataType === 'Point':
      {
        return 'POINT';
      }
    case isDataTypeRelation(dataType):
      {
        return 'RELATION';
      }
    default:
      {
        return 'STRING';
      }
  }
}
//# sourceMappingURL=Mapping.js.map