import { VIRTUAL_PROP_NAMES } from '../../virtual-props.js'
import { isPlainObject } from '../../utils/type-guards.js'
import { getParentContextValue } from '../equality-value-identity.js'
import type { NamespacedRow } from '../../types.js'

const ROUTED_SCALAR_VALUE = Symbol(`tanstack_db_routed_scalar_value`)
const ROUTE_METADATA = Symbol(`tanstack_db_route_metadata`)
export const INCLUDES_PUBLIC_KEY = Symbol(`includesPublicKey`)
export const SOURCE_METADATA = Symbol(`sourceMetadata`)
const SOURCE_ROUTING = Symbol(`sourceRouting`)
export const ROW_METADATA = Symbol(`rowMetadata`)
export type RowMetadata = Partial<
  Record<(typeof VIRTUAL_PROP_NAMES)[number], unknown>
>

export const INCLUDES_ROUTING = Symbol(`includesRouting`)
const INTERNAL_ROUTE_KEYS = new Set<PropertyKey>([
  ROUTE_METADATA,
  INCLUDES_PUBLIC_KEY,
  SOURCE_METADATA,
  ROW_METADATA,
  SOURCE_ROUTING,
])
const INTERNAL_CALLBACK_KEYS = new Set<PropertyKey>([
  ...INTERNAL_ROUTE_KEYS,
  INCLUDES_ROUTING,
])

type RoutedResult = {
  [ROUTED_SCALAR_VALUE]: unknown
  [ROUTE_METADATA]: RouteMetadata
  [INCLUDES_PUBLIC_KEY]: unknown
}

type PublicContainer = {
  value: object
  properties: Record<
    PropertyKey,
    PropertyDescriptor & { child?: PublicContainer }
  >
  parents: Array<PublicContainer>
  copy?: object
}

export type RouteMetadata = {
  correlationKey: unknown
  parentContext: unknown
}

export type RoutedScalarMetadata = {
  value: unknown
  correlationKey: unknown
  parentContext: unknown
  publicKey: unknown
}

export function attachRouteMetadata<T extends object>(
  value: T,
  correlationKey: unknown,
  parentContext: unknown,
): T {
  return Object.assign(value, {
    [ROUTE_METADATA]: { correlationKey, parentContext } satisfies RouteMetadata,
  })
}

export function getRouteMetadata(value: unknown): RouteMetadata | undefined {
  if (
    value == null ||
    typeof value !== `object` ||
    !(ROUTE_METADATA in value)
  ) {
    return undefined
  }
  return (value as { [ROUTE_METADATA]: RouteMetadata })[ROUTE_METADATA]
}

export function getNamespacedRouteMetadata(
  row: unknown,
  source: string,
): RouteMetadata | undefined {
  return (
    getRouteMetadata(row) ??
    (row != null && typeof row === `object`
      ? getRouteMetadata((row as Record<string, unknown>)[source])
      : undefined)
  )
}

export function attachRouteMetadataToResult(
  value: unknown,
  correlationKey: unknown,
  parentContext: unknown,
  publicKey: unknown,
): unknown {
  if (
    correlationKey === undefined &&
    parentContext === undefined &&
    publicKey === undefined
  ) {
    return value
  }

  return Object.assign(
    isPlainObject(value) ? { ...value } : { [ROUTED_SCALAR_VALUE]: value },
    {
      [ROUTE_METADATA]: { correlationKey, parentContext },
      [INCLUDES_PUBLIC_KEY]: publicKey,
    },
  )
}

export function getRoutedScalarMetadata(
  value: unknown,
): RoutedScalarMetadata | undefined {
  if (
    value == null ||
    typeof value !== `object` ||
    !(ROUTED_SCALAR_VALUE in value)
  ) {
    return undefined
  }

  const routed = value as RoutedResult
  return {
    value: routed[ROUTED_SCALAR_VALUE],
    ...routed[ROUTE_METADATA],
    publicKey: routed[INCLUDES_PUBLIC_KEY],
  }
}

/** Give FROM and JOIN rows the same route and callback namespace. */
export function wrapInputRow(
  alias: string,
  row: unknown,
  metadata?: RowMetadata | null,
  hasInternalState = true,
): NamespacedRow {
  if (!hasInternalState) return { [alias]: row } as NamespacedRow
  const scalar = getRoutedScalarMetadata(row)
  const route = scalar ?? getRouteMetadata(row)
  const namespaced = {
    ...getParentContextValue(route?.parentContext),
    [alias]: scalar ? scalar.value : stripInternalCallbackMetadata(row),
  } as NamespacedRow
  if (route)
    attachRouteMetadata(namespaced, route.correlationKey, route.parentContext)
  if (route)
    Object.assign(namespaced, {
      [INCLUDES_PUBLIC_KEY]:
        scalar?.publicKey ??
        (row as Record<PropertyKey, unknown>)[INCLUDES_PUBLIC_KEY],
    })
  if (metadata !== undefined)
    Object.assign(namespaced, { [SOURCE_METADATA]: { [alias]: metadata } })
  if (row !== null && typeof row === `object` && INCLUDES_ROUTING in row)
    Object.assign(namespaced, {
      [SOURCE_ROUTING]: {
        [alias]: (row as Record<PropertyKey, unknown>)[INCLUDES_ROUTING],
      },
    })
  return namespaced
}

/** Attribution and include routing belong to the namespace, never source data. */
export function getSourceMetadata(
  row: Record<PropertyKey, unknown>,
  alias: string,
): RowMetadata | null {
  const metadata = row[SOURCE_METADATA] as
    Record<string, RowMetadata | null> | undefined
  if (metadata && Object.hasOwn(metadata, alias)) return metadata[alias]!
  const source = row[alias]
  if (source == null || typeof source !== `object`) return null
  const result: RowMetadata = {}
  for (const name of VIRTUAL_PROP_NAMES)
    if (name in source) result[name] = (source as Record<string, unknown>)[name]
  return result
}

export function getSourceRouting(
  row: Record<PropertyKey, unknown>,
  alias: string,
): unknown {
  return (row[SOURCE_ROUTING] as Record<string, unknown> | undefined)?.[alias]
}

export function mergeSourceContext(
  target: Record<PropertyKey, unknown>,
  left: Record<PropertyKey, unknown> | undefined,
  right: Record<PropertyKey, unknown> | undefined,
): void {
  for (const field of [SOURCE_METADATA, SOURCE_ROUTING])
    if (left?.[field] || right?.[field])
      target[field] = {
        ...(left?.[field] as object | undefined),
        ...(right?.[field] as object | undefined),
      }
}

/** Restore namespace state after resolving its public include paths. */
export function restoreInternalRouteMetadata(
  target: Record<PropertyKey, unknown>,
  source: object,
): void {
  for (const key of INTERNAL_ROUTE_KEYS)
    if (Object.hasOwn(source, key))
      target[key] = (source as Record<PropertyKey, unknown>)[key]
}

/** Remove private route state from a known result root. */
export function stripInternalRouteMetadata(value: unknown): unknown {
  return stripInternalMetadata(value, INTERNAL_ROUTE_KEYS)
}

/** Remove compiler-owned keys before invoking user code, at the known scope. */
export function stripInternalCallbackMetadata(value: unknown): unknown {
  return stripInternalMetadata(value, INTERNAL_CALLBACK_KEYS)
}

function stripInternalMetadata(
  value: unknown,
  keys: ReadonlySet<PropertyKey>,
): unknown {
  if (!isPublicContainer(value)) return value
  for (const key of keys)
    if (Object.hasOwn(value, key)) {
      const properties: Record<PropertyKey, PropertyDescriptor> =
        Object.getOwnPropertyDescriptors(value)
      for (const omitted of keys) delete properties[omitted]
      return Object.defineProperties(
        createPublicContainerShell(value),
        properties,
      )
    }
  return value
}

/** Copy only paths changed by a leaf transform or an omitted private key. */
export function transformPublicContainers(
  value: unknown,
  transformLeaf: (value: unknown) => unknown,
  omittedKeys: ReadonlySet<PropertyKey>,
): unknown {
  const rootReplacement = transformLeaf(value)
  if (!Object.is(rootReplacement, value)) return rootReplacement
  if (!isPublicContainer(value)) return value

  const containers = new Map<object, PublicContainer>()
  const queue: Array<PublicContainer> = []
  const markDirty = (container: PublicContainer) => {
    if (container.copy) return
    container.copy = createPublicContainerShell(container.value)
    queue.push(container)
  }
  const visit = (current: object): PublicContainer => {
    const existing = containers.get(current)
    if (existing) return existing
    const container: PublicContainer = {
      value: current,
      properties: Object.getOwnPropertyDescriptors(current),
      parents: [],
    }
    containers.set(current, container)
    for (const key of Reflect.ownKeys(container.properties)) {
      if (omittedKeys.has(key)) {
        delete container.properties[key]
        markDirty(container)
        continue
      }
      const descriptor = container.properties[key]!
      if (!descriptor.enumerable || !(`value` in descriptor)) continue
      const child = descriptor.value
      descriptor.value = transformLeaf(child)
      if (!Object.is(descriptor.value, child)) markDirty(container)
      else if (isPublicContainer(child)) {
        descriptor.child = visit(child)
        descriptor.child.parents.push(container)
      }
    }
    return container
  }
  const root = visit(value)
  for (const container of queue) {
    for (const parent of container.parents) markDirty(parent)
  }
  // Every dirty container has a shell before linking children, so shared
  // references and cycles point to the same copy without a second recursion.
  for (const container of queue) {
    for (const key of Reflect.ownKeys(container.properties)) {
      const descriptor = container.properties[key]!
      if (descriptor.child)
        descriptor.value = descriptor.child.copy ?? descriptor.child.value
    }
    Object.defineProperties(container.copy!, container.properties)
  }
  return root.copy ?? value
}

/** Allocate a descriptor-copy target without losing array prototypes. */
export function createPublicContainerShell(value: object): object {
  const prototype = Object.getPrototypeOf(value)
  return Array.isArray(value)
    ? Object.setPrototypeOf([], prototype)
    : Object.create(prototype)
}

export function isPublicContainer(value: unknown): value is object {
  return Array.isArray(value) || isPlainObject(value)
}
