import {
	RecordId,
	UnknownRecord,
	createMigrationIds,
	createRecordMigrationSequence,
	createRecordType,
} from '@tldraw/store'
import { mapObjectMapValues, uniqueId } from '@tldraw/utils'
import { T } from '@tldraw/validate'
import { TLArrowBinding } from '../bindings/TLArrowBinding'
import { TLBaseBinding, createBindingValidator } from '../bindings/TLBaseBinding'
import { SchemaPropsInfo } from '../createTLSchema'
import { TLPropsMigrations } from '../recordsWithProps'

/**
 * The default set of bindings that are available in the editor.
 * Currently includes only arrow bindings, but can be extended with custom bindings.
 *
 * @example
 * ```ts
 * // Arrow binding connects an arrow to shapes
 * const arrowBinding: TLDefaultBinding = {
 *   id: 'binding:arrow1',
 *   typeName: 'binding',
 *   type: 'arrow',
 *   fromId: 'shape:arrow1',
 *   toId: 'shape:rectangle1',
 *   props: {
 *     terminal: 'end',
 *     normalizedAnchor: { x: 0.5, y: 0.5 },
 *     isExact: false,
 *     isPrecise: true
 *   }
 * }
 * ```
 *
 * @public
 */
export type TLDefaultBinding = TLArrowBinding

/**
 * A type for a binding that is available in the editor but whose type is
 * unknown—either one of the editor's default bindings or else a custom binding.
 * Used internally for type-safe handling of bindings with unknown structure.
 *
 * @example
 * ```ts
 * // Function that works with any binding type
 * function processBinding(binding: TLUnknownBinding) {
 *   console.log(`Processing ${binding.type} binding from ${binding.fromId} to ${binding.toId}`)
 *   // Handle binding properties generically
 * }
 * ```
 *
 * @public
 */
export type TLUnknownBinding = TLBaseBinding<string, object>

/** @public */
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface TLGlobalBindingPropsMap {}

/** @public */
// prettier-ignore
export type TLIndexedBindings = {
	// We iterate over a union of augmented keys and default binding types.
	// This allows us to include (or conditionally exclude or override) the default bindings in one go.
	//
	// In the `as` clause we are filtering out disabled bindings.
	[K in keyof TLGlobalBindingPropsMap | TLDefaultBinding['type'] as K extends TLDefaultBinding['type']
		? K extends keyof TLGlobalBindingPropsMap
			? // if it extends a nullish value the user has disabled this binding type so we filter it out with never
				TLGlobalBindingPropsMap[K] extends null | undefined
				? never
				: K
			: K
		: K]: K extends TLDefaultBinding['type']
			? // if it's a default binding type we need to check if it's been overridden
				K extends keyof TLGlobalBindingPropsMap
				? // if it has been overriden then use the custom binding definition
					TLBaseBinding<K, TLGlobalBindingPropsMap[K]>
				: // if it has not been overriden then reuse existing type aliases for better type display
					Extract<TLDefaultBinding, { type: K }>
			: // use the custom binding definition
				TLBaseBinding<K, TLGlobalBindingPropsMap[K & keyof TLGlobalBindingPropsMap]>
}

/**
 * The set of all bindings that are available in the editor.
 * Bindings represent relationships between shapes, such as arrows connecting to other shapes.
 *
 * You can use this type without a type argument to work with any binding, or pass
 * a specific binding type string (e.g., `'arrow'`) to narrow down to that specific binding type.
 *
 * @example
 * ```ts
 * // Check binding type and handle accordingly
 * function handleBinding(binding: TLBinding) {
 *   switch (binding.type) {
 *     case 'arrow':
 *       // Handle arrow binding
 *       break
 *     default:
 *       // Handle unknown custom binding
 *       break
 *   }
 * }
 *
 * // Narrow to a specific binding type by passing the type as a generic argument
 * function getArrowSourceId(binding: TLBinding<'arrow'>) {
 *   return binding.fromId // TypeScript knows this is a TLArrowBinding
 * }
 * ```
 *
 * @public
 */
export type TLBinding<K extends keyof TLIndexedBindings = keyof TLIndexedBindings> =
	TLIndexedBindings[K]

/**
 * Type for updating existing bindings with partial properties.
 * Only the id and type are required, all other properties are optional.
 *
 * @example
 * ```ts
 * // Update arrow binding properties
 * const bindingUpdate: TLBindingUpdate<TLArrowBinding> = {
 *   id: 'binding:arrow1',
 *   type: 'arrow',
 *   props: {
 *     normalizedAnchor: { x: 0.7, y: 0.3 } // Only update anchor position
 *   }
 * }
 *
 * editor.updateBindings([bindingUpdate])
 * ```
 *
 * @public
 */
export type TLBindingUpdate<T extends TLBinding = TLBinding> = T extends T
	? {
			id: TLBindingId
			type: T['type']
			typeName?: T['typeName']
			fromId?: T['fromId']
			toId?: T['toId']
			props?: Partial<T['props']>
			meta?: Partial<T['meta']>
		}
	: never

/**
 * Type for creating new bindings with required fromId and toId.
 * The id is optional and will be generated if not provided.
 *
 * @example
 * ```ts
 * // Create a new arrow binding
 * const newBinding: TLBindingCreate<TLArrowBinding> = {
 *   type: 'arrow',
 *   fromId: 'shape:arrow1',
 *   toId: 'shape:rectangle1',
 *   props: {
 *     terminal: 'end',
 *     normalizedAnchor: { x: 0.5, y: 0.5 },
 *     isExact: false,
 *     isPrecise: true
 *   }
 * }
 *
 * editor.createBindings([newBinding])
 * ```
 *
 * @public
 */
export type TLBindingCreate<T extends TLBinding = TLBinding> = T extends T
	? {
			id?: TLBindingId
			type: T['type']
			typeName?: T['typeName']
			fromId: T['fromId']
			toId: T['toId']
			props?: Partial<T['props']>
			meta?: Partial<T['meta']>
		}
	: never

/**
 * Branded string type for binding record identifiers.
 * Prevents mixing binding IDs with other types of record IDs at compile time.
 *
 * @example
 * ```ts
 * import { createBindingId } from '@tldraw/tlschema'
 *
 * // Create a new binding ID
 * const bindingId: TLBindingId = createBindingId()
 *
 * // Use in binding records
 * const binding: TLBinding = {
 *   id: bindingId,
 *   type: 'arrow',
 *   fromId: 'shape:arrow1',
 *   toId: 'shape:rectangle1',
 *   // ... other properties
 * }
 * ```
 *
 * @public
 */
export type TLBindingId = RecordId<TLBinding>

/**
 * Migration version identifiers for the root binding record schema.
 * Currently empty as no migrations have been applied to the base binding structure.
 *
 * @example
 * ```ts
 * // Future migrations would be defined here
 * const rootBindingVersions = createMigrationIds('com.tldraw.binding', {
 *   AddNewProperty: 1,
 * } as const)
 * ```
 *
 * @public
 */
export const rootBindingVersions = createMigrationIds('com.tldraw.binding', {} as const)

/**
 * Migration sequence for the root binding record structure.
 * Currently empty as the binding schema has not required any migrations yet.
 *
 * @example
 * ```ts
 * // Migrations would be automatically applied when loading old documents
 * const migratedStore = migrator.migrateStoreSnapshot({
 *   schema: oldSchema,
 *   store: oldStoreSnapshot
 * })
 * ```
 *
 * @public
 */
export const rootBindingMigrations = createRecordMigrationSequence({
	sequenceId: 'com.tldraw.binding',
	recordType: 'binding',
	sequence: [],
})

/**
 * Type guard to check if a record is a TLBinding.
 * Useful for filtering or type narrowing when working with mixed record types.
 *
 * @param record - The record to check
 * @returns True if the record is a binding, false otherwise
 *
 * @example
 * ```ts
 * // Filter bindings from mixed records
 * const allRecords = store.allRecords()
 * const bindings = allRecords.filter(isBinding)
 *
 * // Type guard usage
 * function processRecord(record: UnknownRecord) {
 *   if (isBinding(record)) {
 *     // record is now typed as TLBinding
 *     console.log(`Binding from ${record.fromId} to ${record.toId}`)
 *   }
 * }
 * ```
 *
 * @public
 */
export function isBinding(record?: UnknownRecord): record is TLBinding {
	if (!record) return false
	return record.typeName === 'binding'
}

/**
 * Type guard to check if a string is a valid TLBindingId.
 * Validates that the ID follows the correct format for binding identifiers.
 *
 * @param id - The string to check
 * @returns True if the string is a valid binding ID, false otherwise
 *
 * @example
 * ```ts
 * // Validate binding IDs
 * const maybeBindingId = 'binding:abc123'
 * if (isBindingId(maybeBindingId)) {
 *   // maybeBindingId is now typed as TLBindingId
 *   const binding = store.get(maybeBindingId)
 * }
 *
 * // Filter binding IDs from mixed ID array
 * const mixedIds = ['shape:1', 'binding:2', 'page:3']
 * const bindingIds = mixedIds.filter(isBindingId)
 * ```
 *
 * @public
 */
export function isBindingId(id?: string): id is TLBindingId {
	if (!id) return false
	return id.startsWith('binding:')
}

/**
 * Creates a new TLBindingId with proper formatting.
 * Generates a unique ID if none is provided, or formats a provided ID correctly.
 *
 * @param id - Optional custom ID suffix. If not provided, a unique ID is generated
 * @returns A properly formatted binding ID
 *
 * @example
 * ```ts
 * // Create with auto-generated ID
 * const bindingId1 = createBindingId() // 'binding:abc123'
 *
 * // Create with custom ID
 * const bindingId2 = createBindingId('myCustomBinding') // 'binding:myCustomBinding'
 *
 * // Use in binding creation
 * const binding: TLBinding = {
 *   id: createBindingId(),
 *   type: 'arrow',
 *   fromId: 'shape:arrow1',
 *   toId: 'shape:rectangle1',
 *   // ... other properties
 * }
 * ```
 *
 * @public
 */
export function createBindingId(id?: string): TLBindingId {
	return `binding:${id ?? uniqueId()}` as TLBindingId
}

/**
 * Creates a migration sequence for binding properties.
 * This is a pass-through function that validates and returns the provided migrations.
 *
 * @param migrations - The migration sequence for binding properties
 * @returns The validated migration sequence
 *
 * @example
 * ```ts
 * // Define migrations for custom binding properties
 * const myBindingMigrations = createBindingPropsMigrationSequence({
 *   sequence: [
 *     {
 *       id: 'com.myapp.binding.custom/1.0.0',
 *       up: (props) => ({ ...props, newProperty: 'default' }),
 *       down: ({ newProperty, ...props }) => props
 *     }
 *   ]
 * })
 * ```
 *
 * @public
 */
export function createBindingPropsMigrationSequence(
	migrations: TLPropsMigrations
): TLPropsMigrations {
	return migrations
}

/**
 * Creates properly formatted migration IDs for binding property migrations.
 * Follows the convention: 'com.tldraw.binding.\{bindingType\}/\{version\}'
 *
 * @param bindingType - The type of binding these migrations apply to
 * @param ids - Object mapping migration names to version numbers
 * @returns Object with formatted migration IDs
 *
 * @example
 * ```ts
 * // Create migration IDs for custom binding
 * const myBindingVersions = createBindingPropsMigrationIds('myCustomBinding', {
 *   AddNewProperty: 1,
 *   UpdateProperty: 2
 * })
 *
 * // Result:
 * // {
 * //   AddNewProperty: 'com.tldraw.binding.myCustomBinding/1',
 * //   UpdateProperty: 'com.tldraw.binding.myCustomBinding/2'
 * // }
 * ```
 *
 * @public
 */
export function createBindingPropsMigrationIds<S extends string, T extends Record<string, number>>(
	bindingType: S,
	ids: T
): { [k in keyof T]: `com.tldraw.binding.${S}/${T[k]}` } {
	return mapObjectMapValues(ids, (_k, v) => `com.tldraw.binding.${bindingType}/${v}`) as any
}

/**
 * Creates a record type for TLBinding with validation based on the provided binding schemas.
 * This function is used internally to configure the binding record type in the schema.
 *
 * @param bindings - Record mapping binding type names to their schema information
 * @returns A configured record type for bindings with validation
 *
 * @example
 * ```ts
 * // Used internally when creating schemas
 * const bindingRecordType = createBindingRecordType({
 *   arrow: {
 *     props: arrowBindingProps,
 *     meta: arrowBindingMeta
 *   }
 * })
 * ```
 *
 * @internal
 */
export function createBindingRecordType(bindings: Record<string, SchemaPropsInfo>) {
	return createRecordType('binding', {
		scope: 'document',
		validator: T.model(
			'binding',
			T.union(
				'type',
				mapObjectMapValues(bindings, (type, { props, meta }) =>
					createBindingValidator(type, props, meta)
				)
			)
		),
	}).withDefaultProperties(() => ({
		meta: {},
	}))
}
