import { Component } from 'grapesjs'
import { Expression, Field, FieldArgument, Filter, Options, Property, PropertyOptions, StoredToken, Token, Type } from '../types'
import { getFilters, getManager } from './dataSourceManager'
import { NOTIFICATION_GROUP } from '../utils'
import { getParentByPersistentId, getState } from './state'
import { TemplateResult, html } from 'lit'

/**
 * Add missing methonds to the filter
 * When filters are stored they lose their methods
 * @throws Error if the filter is not found
 */
export function getFilterFromToken(token: Filter, filters: Filter[]): Filter {
  if(token.type !== 'filter') throw new Error('Token is not a filter')
  const filter = filters.find(filter => filter.id === token.id)
  if (!filter) {
    console.error('Filter not found', token)
    throw new Error(`Filter ${token.id} not found`)
  }
  return {
    ...token,
    ...filter,
    // Keep the options as they are stored
    options: token.options,
  }
}

/**
 * Get the token from its stored form
 * @throws Error if the token type is not found
 */
export function fromStored<T extends Token = Token>(token: StoredToken, componentId: string | null): T {
  // Handle invalid tokens - return null or throw a descriptive error
  if (!token || typeof token !== 'object') {
    console.error('Invalid token: not an object', token)
    throw new Error('Invalid token: expected an object')
  }

  if (!token.type) {
    console.error('Invalid token: missing type property', token)
    throw new Error('Invalid token: missing type property')
  }

  switch (token.type) {
  case 'filter': {
    if ((token as Filter).optionsForm) return token as T
    const original = getFilters().find(filter => filter.id === token.id) as T | undefined
    if (!original) {
      console.error('Filter not found', token)
      throw new Error(`Filter ${token.id} not found`)
    }
    return {
      ...original,
      ...token,
      type: 'filter',
    } as T
  }
  case 'property': {
    if ((token as Property).optionsForm) return token as T
    const field = propertyToField(token, componentId)
    if (!field) {
      console.error('Field not found for token', token)
      throw new Error(`Field ${token.fieldId} not found`)
    }
    return {
      ...getTokenOptions(field) ?? {},
      ...token,
      type: 'property',
      propType: 'field',
    } as T
  }
  case 'state':
    return token as T
  default:
    console.error('Unknown token type (reading type)', token)
    throw new Error('Unknown token type')
  }
}

/**
 * Get the type corresponding to a token
 */
export function tokenToField(token: Token, prev: Field | null, component: Component): Field | null {
  switch (token.type) {
  case 'filter': {
    try {
      const filter = getFilterFromToken(token, getFilters())
      try {
        if (filter.validate(prev)) {
          return filter.output(prev, filter.options ?? {})
        }
      } catch (e) {
        console.warn('Filter validate error:', e, {token, prev})
        return null
      }
      return null
    } catch {
      // FIXME: notify user
      console.error('Error while getting filter result type', {token, prev, component})
      return null
    }
  }
  case 'property':
    try {
      return propertyToField(token, component.getId())
    } catch {
      // FIXME: notify user
      console.error('Error while getting property result type', {token, component})
      return null
    }
  case 'state': {
    const parent = getParentByPersistentId(token.componentId, component)
    if (!parent) {
      console.warn('Component not found for state', token)
      const manager = getManager()
      manager.editor.runCommand('notifications:add', {
        type: 'error',
        group: NOTIFICATION_GROUP,
        message: `Component not found for state: ${token.storedStateId}`,
        componentId: component.getId(),
      })
      return null
    }
    const expression = getState(parent, token.storedStateId, token.exposed)?.expression
    if (!expression) {
      console.warn('State is not defined on component', { component: parent, token })
      const manager = getManager()
      manager.editor.runCommand('notifications:add', {
        type: 'error',
        group: NOTIFICATION_GROUP,
        message: `State '${token.storedStateId}' is not defined on component '${parent.getName() || parent.get('id')}'`,
        componentId: parent.getId(),
      })
      return null
    }
    try {
      const field = getExpressionResultType(expression, parent)
      return field ? {
        ...field,
        kind: token.forceKind ?? field.kind,
      } : null
    } catch {
      // FIXME: notify user
      console.error('Error while getting expression result type in tokenToField', {expression, parent, component, token, prev})
      return null
    }
  }
  default:
    console.error('Unknown token type (reading type)', token)
    throw new Error('Unknown token type')
  }
}

/**
 * Get the type corresponding to a property
 * @throws Error if the type is not found
 */
export function propertyToField(property: Property, componentId: string | null): Field {
  const manager = getManager()
  const allTypes = manager.cachedTypes.length > 0 ? manager.cachedTypes : []

  const typeNames: string[] = []

  // Check each typeId and handle missing types
  for (const typeId of property.typeIds) {
    const type: Type | undefined = allTypes.find(type => type.id === typeId && (!property.dataSourceId || type.dataSourceId === property.dataSourceId))
    if (!type) {
      // Show notification for missing type, similar to main branch DataTree.getType
      manager.editor.runCommand('notifications:add', {
        type: 'error',
        group: NOTIFICATION_GROUP,
        message: `Type not found ${property.dataSourceId ?? ''}.${typeId}`,
        componentId,
      })
      console.error(`Type not found ${property.dataSourceId ?? ''}.${typeId}`)
      // Continue processing other types instead of throwing
    } else {
      typeNames.push(type.label)
    }
  }

  const args = property.options ? Object.entries(property.options).map(([name, value]) => ({
    typeId: 'JSON', // FIXME: Why is this hardcoded?
    name,
    defaultValue: value, // FIXME: Why is this value, it should keep the initial default
  })) : undefined

  return {
    id: property.fieldId,
    label: typeNames.length > 0 ? typeNames.join(', ') : property.label,
    typeIds: property.typeIds,
    kind: property.kind,
    dataSourceId: property.dataSourceId,
    arguments: args,
    previewIndex: property.previewIndex,
  }
}

/**
 * Evaluate the types of each token in an expression
 */
export function expressionToFields(expression: Expression, component: Component): Field[] {
  // Resolve type of the expression 1 step at a time
  let prev: Field | null = null
  const unknownField: Field = {
    id: 'unknown',
    label: 'unknown',
    typeIds: [],
    kind: 'scalar',
  }
  return expression.map((token) => {
    try {
      const field = tokenToField(fromStored(token, component.getId()), prev, component)
      if (!field) {
        console.warn('Type not found for token in expressionToFields', { token, expression })
        return unknownField
      }
      prev = field
      return field
    } catch {
      // FIXME: notify user
      console.error('Error while getting expression result type in expressionToFields', {expression, component, token, prev})
      return unknownField
    }
  })
}

/**
 * Evaluate an expression to a type
 * This is used to validate expressions and for autocompletion
 * @throws Error if the token type is not found
 */
export function getExpressionResultType(expression: Expression, component: Component): Field | null {
  // Resolve type of the expression 1 step at a time
  return expression.reduce((prev: Field | null, token: StoredToken) => {
    return tokenToField(fromStored(token, component.getId()), prev, component)
  }, null)
}

/**
 * Get the options of a token
 */
export function getTokenOptions(field: Field): { optionsForm: (selected: Component, input: Field | null, options: Options) => TemplateResult, options: Options } | null {
  if (field.arguments && field.arguments.length > 0) {
    return {
      optionsForm: optionsToOptionsForm(field.arguments.map((arg) => ({ name: arg.name, value: arg.defaultValue }))),
      options: field.arguments.reduce((options: Record<string, unknown>, arg: FieldArgument) => {
        options[arg.name] = arg.defaultValue
        return options
      }, {}),
    }
  }
  return null
}

/**
 * Get the options of a token or a field
 */
export function optionsToOptionsForm(arr: { name: string, value: unknown }[]): (selected: Component, input: Field | null, options: Options) => TemplateResult {
  return (selected: Component, input: Field | null, options: Options) => {
    return html`
              ${arr.map((obj) => {
    const value = options[obj.name] ?? obj.value ?? ''
    return html`<label>${obj.name}</label><input type="text" name=${obj.name} .value=${value}>`
  })
}
          `
  }
}

/**
 * Utility function to shallow compare two objects
 * Used to compare options of tree items
 */
export function getOptionObject(option1: PropertyOptions | undefined, option2: PropertyOptions | undefined): { error: boolean, result: PropertyOptions | undefined } {
  // Handle the case where one or both are undefined or empty
  if(!option1 && !option2) return { error: false, result: undefined }
  if(isEmpty(option1) && isEmpty(option2)) return { error: false, result: undefined }
  // Handle the case where one is undefined or empty and the other is not
  if(!option1 || !option2) return { error: true, result: undefined }
  if(isEmpty(option1) || isEmpty(option2)) return { error: true, result: undefined }

  const keys1 = Object.keys(option1)
  const keys2 = Object.keys(option2)

  if (keys1.length !== keys2.length) {
    return { error: true, result: undefined }
  }

  for (const key of keys1) {
    if (option1[key] !== option2[key]) {
      return { error: true, result: undefined }
    }
  }

  return { error: false, result: option1 }
}

function isJson(str: string) {
  if(typeof str !== 'string') return false
  if(str.length === 0) return false
  try {
    JSON.parse(str)
  } catch {
    return false
  }
  return true
}

function isEmpty(value: unknown): boolean {
  if(value === null || typeof value === 'undefined') return true
  const isString = typeof value === 'string'
  const isJsonString = isString && isJson(value)
  if (isString && !isJsonString) return value === ''
  const json = isJsonString ? JSON.parse(value) : value
  if (json === null) return true
  if (Array.isArray(json)) return json.length === 0
  if (typeof json === 'object') return Object.values(json).filter(v => !!v).length === 0
  return false
}

export function buildArgs(options: PropertyOptions | undefined): string {
  const args = options ? `(${Object
    .keys(options)
    .map(key => ({ key, value: options![key] }))
    .filter(({ value }) => !isEmpty(value))
    .map(({ key, value }) => {
      //return typeof value === 'string' && !isJson(value) ? `${key}: "${value}"` : `${key}: ${value}`
      return `${key}: ${value}`
    })
    .join(', ')
  })` : ''
  // Valid args for GraphQL canot be just ()
  const validArgs = args === '()' ? '' : args
  return validArgs
}
