import React, {
  useContext,
  JSXElementConstructor,
  ReactNode,
  createContext,
} from 'react'

import type { ContentTree } from '@financial-times/content-tree'
import Recommended from '../content-tree/Recommended'
import RecommendedList from '../content-tree/RecommendedList'
import ImageSet, { FallbackImage } from '../content-tree/ImageSet'
import { Layout, LayoutSlot } from '../content-tree/Layout'
import Pullquote from '../content-tree/Pullquote'
import Tweet from '../content-tree/Tweet'
import Flourish from '../content-tree/Flourish'
import Heading from '../content-tree/Heading'
import BigNumber from '../content-tree/BigNumber'
import Video from '../content-tree/Video'
import Table from '../content-tree/Table'
import Clip from '../content-tree/Clip'
import Paragraph from '../content-tree/Paragraph'
import TableBody from '../content-tree/Table/TableBody'
import { TableCell } from '../content-tree/Table/TableCell'
import {
  ScrollyBlock,
  ScrollyCopy,
  ScrollyHeading,
  ScrollySection,
} from '../content-tree/Scrollytelling'
import { ScrollyImage } from '../content-tree/Scrollytelling/ScrollyImage'
import YoutubeVideo from '../content-tree/YoutubeVideo'
import CustomCodeComponent from '../content-tree/CustomCodeComponent'
import ImagePair from '../content-tree/ImagePair'
import Timeline from '../content-tree/Timeline'
import TimelineEvent from '../content-tree/Timeline/TimelineEvent'
import Card from '../content-tree/Card'
import InfoBox from '../content-tree/InfoBox'
import InfoPair from '../content-tree/InfoPair'
import InNumbers, { Definition } from '../content-tree/InNumbers'

import {
  List,
  ListItem,
  Link,
  Blockquote,
  Cite,
  LineBreak,
  HorizontalRule,
  Emphasis,
  Strong,
  Strikethrough,
  TableCaption,
  TableFooter,
  TableRow,
  CccFallbackText,
} from './BasicComponents'

import type {
  StructuredContentFragment,
  ReferenceMapping,
} from '@financial-times/cp-content-pipeline-client'
import type { ContentTreeWorkarounds } from '@financial-times/cp-content-pipeline-schema'
import type { mapNodeToReference } from '@financial-times/cp-content-pipeline-schema/lib/resolvers/content-tree/references'
import type { ContentProps } from '../types'

import RichTextContext from './context'

// A helper type that takes a content tree type identifier and maps it to the
// object that is returned in an accompanying reference
type mapComponentToReference<T extends ContentTreeWorkarounds.AnyNode['type']> =
  T extends keyof typeof mapNodeToReference
    ? (typeof mapNodeToReference)[T] extends keyof ReferenceMapping
      ? ReferenceMapping[(typeof mapNodeToReference)[T]]
      : never
    : Record<string, never>

// Partial record of content tree nodes and their associated reference types
export type RichTextComponentMapRecord = {
  [T in
    | ContentTreeWorkarounds.AnyNode
    | { type: 'fallback' } as T['type']]?: JSXElementConstructor<
    React.PropsWithChildren<
      T['type'] extends 'fallback'
        ? unknown
        : ContentProps<
            T & mapComponentToReference<Exclude<T['type'], 'fallback'>>
          >
    >
  >
}

export const ComponentsContext = createContext<
  RichTextComponentMapRecord | undefined
>({})

type WithOptionalProperty<T, K extends keyof T> = Pick<Partial<T>, K> &
  Omit<T, K>

export type RichTextProps = {
  // the component using RichText might not have queried for references, so make it optional
  structuredContent: WithOptionalProperty<
    StructuredContentFragment,
    'references'
  >
  components?: RichTextComponentMapRecord
}

/* This will be the default set of components we provide, that most
 * clients can use to render a workable article page
 */

const componentMap: RichTextComponentMapRecord = {
  fallback: (props) => props.children,
  'big-number': BigNumber,
  cite: Cite,
  blockquote: Blockquote,
  break: LineBreak,
  'ccc-fallback-text': CccFallbackText,
  'clip-set': Clip,
  clip: Clip,
  'custom-code-component': CustomCodeComponent,
  emphasis: Emphasis,
  flourish: Flourish,
  heading: Heading,
  'image-pair': ImagePair,
  'image-set': ImageSet,
  'layout-image': ImageSet,
  'layout-slot': LayoutSlot,
  layout: Layout,
  link: Link,
  'list-item': ListItem,
  list: List,
  paragraph: Paragraph,
  pullquote: Pullquote,
  'raw-image': FallbackImage,
  recommended: Recommended,
  'recommended-list': RecommendedList,
  'scrolly-block': ScrollyBlock,
  'scrolly-copy': ScrollyCopy,
  'scrolly-heading': ScrollyHeading,
  'scrolly-image': ScrollyImage,
  'scrolly-section': ScrollySection,
  strikethrough: Strikethrough,
  strong: Strong,
  'table-body': TableBody,
  'table-caption': TableCaption,
  'table-cell': TableCell,
  'table-footer': TableFooter,
  'table-row': TableRow,
  table: Table,
  'thematic-break': HorizontalRule,
  tweet: Tweet,
  video: Video,
  'youtube-video': YoutubeVideo,
  timeline: Timeline,
  'timeline-event': TimelineEvent,
  card: Card,
  'info-box': InfoBox,
  'info-pair': InfoPair,
  'in-numbers': InNumbers,
  definition: Definition,
}

function isParentNode(
  node: ContentTreeWorkarounds.AnyNode | ContentTree.Node
): node is ContentTree.Parent {
  return 'children' in node
}

type RichTextChildProps = {
  node: ContentTreeWorkarounds.AnyNode
  components: RichTextComponentMapRecord
  references: StructuredContentFragment['references']
  parentIndex: number
}

const RichTextChild: React.FC<RichTextChildProps> = ({
  node,
  components,
  references,
  parentIndex,
}) => {
  if (node.type === 'text') {
    return <>{node.value}</>
  }

  let Component = components[node.type]
  if (!Component) {
    console.warn(
      `couldn't find component for Content Tree node ${node.type}, using fallback component instead (by default will render the node's children)`
    )
    Component = components.fallback
  }

  if (Component) {
    const children: ReactNode[] = isParentNode(node)
      ? node.children
          .map((child, index) => (
            <RichTextChild
              node={child as ContentTreeWorkarounds.AnyNode}
              key={index}
              parentIndex={index}
              components={components}
              references={references}
            />
          ))
          .filter(Boolean)
      : []

    const reference:
      | StructuredContentFragment['references'][number]
      | Record<string, never> =
      typeof node.data?.referenceIndex === 'number'
        ? references[node.data?.referenceIndex] ?? {}
        : {}

    return (
      <RichTextContext.Provider value={RichText}>
        <Component
          // HACK:20240808:IM TypeScript fails to unify the content types here
          // and I'm not sure why :(
          content={{ ...node, ...reference } as never}
          key={parentIndex}
          parentIndex={parentIndex}
        >
          {children}
        </Component>
      </RichTextContext.Provider>
    )
  }

  return null
}

const RichText: React.FC<RichTextProps> = ({
  structuredContent,
  components,
}) => {
  if (!structuredContent) {
    return null
  }

  const componentsWithOverrides = {
    ...componentMap,
    ...useContext(ComponentsContext),
    ...components,
  }

  const tree: ContentTree.Body = structuredContent.tree

  return (
    <>
      {tree?.children?.map((node, index) => (
        <RichTextChild
          node={node as ContentTreeWorkarounds.AnyNode}
          parentIndex={index}
          key={index}
          components={componentsWithOverrides}
          references={structuredContent.references ?? []}
        />
      ))}
    </>
  )
}

export default RichText
