import { and, eq, isNull, or } from 'drizzle-orm'
import type { Querier } from '@platform-modules/db'
import type {
  EntityType,
  FieldDefinition,
  FieldGroup,
  FieldValue,
  FieldValueMap,
  MediaRef,
  RelRef,
  ResolvedFieldGroup,
} from './model.js'
import { FIELD_KEY_RE } from './model.js'
import { FieldGroupConflictError, FieldValidationError } from './errors.js'
import { fieldGroups, type FieldsSchema } from './schema.js'

export type CodeFieldGroup = FieldGroup & { readonly origin: 'code' }

function matchesLocation(entityType: EntityType, subType: string | undefined, rule: FieldGroup['location']): boolean {
  if (rule.entityType !== entityType) return false
  if (rule.subType === undefined) return true
  if (subType === undefined) return false
  return rule.subType === subType
}

type ScalarInfer<F extends FieldDefinition> = F extends { type: 'text' | 'textarea' | 'color' | 'url' | 'email' }
  ? string
  : F extends { type: 'number' }
    ? number
    : F extends { type: 'boolean' }
      ? boolean
      : F extends { type: 'date' }
        ? Date
        : F extends { type: 'media' }
          ? MediaRef
          : F extends { type: 'relationship' }
            ? RelRef
            : F extends { type: 'select' }
              ? string
              : never

type FieldValueInfer<F extends FieldDefinition> = F extends { multiple: true }
  ? ScalarInfer<F>[]
  : ScalarInfer<F>

export type InferValues<G extends CodeFieldGroup> = {
  [F in G['fields'][number] as F['key']]: FieldValueInfer<F>
}

function validateGroupShape(group: FieldGroup): void {
  if (!FIELD_KEY_RE.test(group.key)) {
    throw new FieldValidationError(group.key, 'invalid group key')
  }
  const seen = new Set<string>()
  for (const f of group.fields) {
    if (!FIELD_KEY_RE.test(f.key)) throw new FieldValidationError(f.key, 'invalid field key')
    if (seen.has(f.key)) throw new FieldGroupConflictError(group.key, `duplicate field key ${f.key}`)
    seen.add(f.key)
    if (f.type === 'select' && (!f.options || f.options.length === 0)) {
      throw new FieldValidationError(f.key, 'select requires options')
    }
    if (f.type === 'relationship' && !f.targetEntityType) {
      throw new FieldValidationError(f.key, 'relationship requires targetEntityType')
    }
  }
}

export function defineFieldGroup(group: FieldGroup): CodeFieldGroup {
  validateGroupShape(group)
  return { ...group, origin: 'code' } as CodeFieldGroup
}

export function assertNoCrossGroupFieldKeyConflict(groups: ResolvedFieldGroup[]): void {
  const fieldOwners = new Map<string, string>()
  for (const g of groups) {
    for (const f of g.fields) {
      const prev = fieldOwners.get(f.key)
      if (prev !== undefined && prev !== g.key) {
        throw new FieldGroupConflictError(f.key, `field key ${f.key} defined in groups ${prev} and ${g.key}`)
      }
      fieldOwners.set(f.key, g.key)
    }
  }
}

export function assertNoCodeKeyShadow(
  dbKey: string,
  entityType: EntityType,
  codeGroups: CodeFieldGroup[] | undefined,
): void {
  const shadow = codeGroups?.find((g) => g.location.entityType === entityType && g.key === dbKey)
  if (shadow) throw new FieldGroupConflictError(dbKey, 'shadows code key')
}

export async function resolveGroups(
  db: Querier<FieldsSchema>,
  opts: { entityType: EntityType; subType?: string; codeGroups?: CodeFieldGroup[] },
): Promise<ResolvedFieldGroup[]> {
  const { entityType, subType, codeGroups = [] } = opts

  const dbRows = await db
    .select()
    .from(fieldGroups)
    .where(
      and(
        eq(fieldGroups.entityType, entityType),
        subType === undefined
          ? isNull(fieldGroups.subType)
          : or(isNull(fieldGroups.subType), eq(fieldGroups.subType, subType)),
      ),
    )

  const dbResolved: ResolvedFieldGroup[] = dbRows.map((row) => ({
    key: row.key,
    label: row.label,
    location: { entityType: row.entityType, subType: row.subType ?? undefined },
    fields: row.fields as FieldDefinition[],
    position: row.position,
    origin: 'db' as const,
    id: row.id,
  }))

  const codeResolved: ResolvedFieldGroup[] = codeGroups
    .filter((g) => matchesLocation(entityType, subType, g.location))
    .map((g) => ({ ...g, origin: 'code' as const }))

  for (const dbG of dbResolved) {
    assertNoCodeKeyShadow(dbG.key, entityType, codeGroups)
  }

  const merged = [...codeResolved, ...dbResolved].sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
  assertNoCrossGroupFieldKeyConflict(merged)
  return merged
}

export function typedValues<G extends CodeFieldGroup>(group: G, map: FieldValueMap): InferValues<G> {
  const out: Record<string, FieldValue | undefined> = {}
  for (const f of group.fields) {
    if (f.key in map) out[f.key] = map[f.key]
  }
  return out as InferValues<G>
}
