import find from './dom/find'
import findAll from './dom/findAll'
import onFind from './dom/onFind'
import onFindOnce from './dom/onFindOnce'
import Tether from './dom/Tether'
import Cookies from 'js-cookie'
import rtc from './rtc'
import { setupMenuForAria, toggleMenu } from './dom/popupMenu'

const conversationPopups: string[] = []

interface PostBroadcastData {
  postId: string
  contentId: string
  parentId: string
  fieldName: string
  overlayId: string
  isDelete: boolean
  fieldHasUnresolvedComments: boolean
}

// Constants for better maintainability
const TIMING = {
  ANIMATION_DURATION: 1000,
  DOM_UPDATE_DELAY: 100,
  HIGHLIGHT_DURATION: 2000,
  TYPING_DEBOUNCE: 500,
  TOGGLE_RESET_DELAY: 1000,
} as const

onFind(
  ['.Conversation.frame.loaded', '.Popup .Conversation'],
  (conversation: HTMLElement): void => {
    const data = conversation?.dataset
    const contentId = data.contentId
    const parentId = data.parentId || ''
    const fieldName = data.fieldName || ''
    const contextual = data.contextual || 'false'
    const overlayId = data.overlayId || ''
    const isPopup = conversation.closest('.Popup') !== null

    if (
      !conversationPopups.includes(parentId) ||
      !conversationPopups.includes(fieldName)
    ) {
      rtc.receive(
        'com.psddev.cms.conversation.PostBroadcast',
        async (data: unknown): Promise<void> => {
          const postData = data as PostBroadcastData
          if (
            contentId === postData.contentId &&
            (parentId === postData.parentId || parentId === '') &&
            (fieldName === postData.fieldName || fieldName === '')
          ) {
            // Fetch updated posts and replace the posts container
            const url =
              window.BRIGHTSPOT.web.pagePaths[
                'com.psddev.cms.conversation.Posts'
              ] +
              '?' +
              new URLSearchParams({
                postsOnly: 'true',
                contentId,
                parentId,
                fieldName,
                contextual,
                overlayId,
              })
            const response = await fetch(url, {
              credentials: 'include',
            })

            const responseText = await response.text()
            const posts = new DOMParser()
              .parseFromString(responseText, 'text/html')
              .body.querySelector('.Conversation-posts')
            const conversationPosts = find(conversation, '.Conversation-posts')

            if (conversationPosts && posts) {
              conversationPosts.replaceWith(posts)

              // Indicate conversation has comments by changing icon
              const commentIcon = conversation
                .closest('.ContentEdit')
                ?.querySelector(
                  `.CIG-comments[data-field-name="${postData.fieldName}"]`,
                )
              if (commentIcon) {
                commentIcon.classList.add('has-comments')
              }
            }

            if (isPopup) {
              const form = conversation.querySelector('.Conversation-newPost')
              if (form) {
                form.scrollIntoView({ behavior: 'smooth', block: 'end' })
                setTimeout(() => {
                  form.scrollIntoView({ behavior: 'smooth', block: 'end' })
                }, 500)
              }
            } else {
              const post = document.getElementById(`${postData.postId}`)
              if (post && document.body.contains(post)) {
                post.scrollIntoView({ behavior: 'smooth', block: 'end' })
              }
            }
          }

          // Toggle the fields 'CIG-comments' to reflect the current state
          if (postData.fieldName) {
            const selector = `.CIG-row[data-name="${postData.fieldName}"] > .CIG-label > .CIG-comments`
            const commentsIcon = document.querySelector(selector)
            if (commentsIcon) {
              commentsIcon.classList.toggle(
                'has-comments',
                postData.fieldHasUnresolvedComments,
              )
            } else {
              onFindOnce(selector, () => {
                document
                  .querySelector(selector)
                  ?.classList.toggle(
                    'has-comments',
                    postData.fieldHasUnresolvedComments,
                  )
              })
            }
          }
        },
      )

      // If the popup has already been added to the list then hide the title that is written in the new frame
      if (
        conversationPopups.includes(parentId) ||
        conversationPopups.includes(fieldName)
      ) {
        const popup = conversation.closest('.Popup[name="Contextual-comments"]')
        if (popup instanceof HTMLElement) {
          const title = find(
            popup,
            '.frame.loaded[name="Contextual-comments"] .Widget-title',
          )
          if (title) {
            title.classList.add('!hidden')
          }
        }
      }
      if (parentId) {
        conversationPopups.push(parentId)
      }

      if (fieldName) {
        conversationPopups.push(fieldName)
      }
    }

    if (isPopup) {
      const form = conversation.querySelector('.Conversation-newPost')
      if (form) {
        form.scrollIntoView({ behavior: 'smooth', block: 'end' })
        setTimeout(() => {
          form.scrollIntoView({ behavior: 'smooth', block: 'end' })
        }, 700)
      }
    } else {
      if (conversation.closest('.ContentEdit-left')) return
      const postsContainer = conversation.querySelector<HTMLElement>(
        '.Conversation-posts',
      )
      if (!postsContainer) return
      setTimeout(() => {
        postsContainer.lastElementChild?.scrollIntoView({
          behavior: 'smooth',
          block: 'end',
        })
      }, 700)
    }
  },
)

onFind('.ContentEdit-workflowBody', (conversation: HTMLElement): void => {
  const workflowActions = findAll(
    conversation,
    '.ContentEdit-workflowActions > [name=action-workflow]',
  )
  for (const action of workflowActions) {
    action.addEventListener('click', async (event) => {
      const headers = new Headers()
      const cookie = Cookies.get('bsp.csrf')

      if (cookie) {
        headers.append('Brightspot-CSRF', cookie)
      }

      const body = new FormData()
      const conversation = action.closest<HTMLElement>('.ContentEdit')
      const newPost = find(
        <HTMLElement>conversation,
        '.Conversation-workflow > .CIG > .CIG-row > .CIG-small',
      )
      const workflowLogId = find(
        <HTMLElement>conversation,
        '.Conversation-workflow > [name=workflowLogId]',
      )
      let newPostText

      if (newPost) {
        newPostText = (<HTMLElement>newPost).dataset.value

        if (newPostText) {
          body.append('postText', <string>newPostText)
        }
      }

      if (conversation) {
        const contentId = conversation.dataset.contentId
        const workflowId = (<HTMLInputElement>workflowLogId).value
        const overlayId = conversation.dataset.overlayId
        if (contentId) body.append('contentId', contentId)
        if (workflowLogId) body.append('workflowLogId', workflowId)
        if (overlayId) body.append('overlayId', overlayId)
      }

      body.append('postsOnly', 'true')

      await fetch(
        window.BRIGHTSPOT.web.pagePaths['com.psddev.cms.conversation.Posts'],
        {
          method: 'post',
          credentials: 'include',
          headers,
          body,
        },
      )
    })
  }
})

onFind('.Conversation-newPostButton', (button: HTMLElement): void => {
  button.addEventListener('click', async (event) => {
    event.preventDefault()

    const headers = new Headers()
    const cookie = Cookies.get('bsp.csrf')

    if (cookie) {
      headers.append('Brightspot-CSRF', cookie)
    }

    const body = new FormData()
    const conversation = button.closest<HTMLElement>('.Conversation')
    const newPost = button.closest<HTMLElement>('.Conversation-newPost')
    let newPostText

    if (newPost) {
      newPostText = find(newPost, '[name="text"]')

      if (newPostText instanceof HTMLTextAreaElement) {
        body.append('postText', newPostText.value)
      }
    }

    if (conversation) {
      const data = conversation.dataset
      body.append('contentId', data.contentId || '')
      body.append('parentId', data.parentId || '')
      body.append('fieldName', data.fieldName || '')
      body.append('contextual', data.contextual || 'false')
      body.append('overlayId', data.overlayId || '')
    }

    body.append('postsOnly', 'true')

    const response = await fetch(
      window.BRIGHTSPOT.web.pagePaths['com.psddev.cms.conversation.Posts'],
      {
        method: 'post',
        credentials: 'include',
        headers,
        body,
      },
    )

    if (conversation) {
      const responseText = await response.text()
      const posts = new DOMParser()
        .parseFromString(responseText, 'text/html')
        .body.querySelector('.Conversation-posts')
      const conversationPosts = find(conversation, '.Conversation-posts')

      if (conversationPosts && posts) {
        conversationPosts.replaceWith(posts)
        conversationPosts.lastElementChild?.scrollIntoView({
          behavior: 'smooth',
          block: 'end',
        })
      }

      const rteParent = conversation.querySelector('.ProseMirrorContainer')
      const clearRte = new CustomEvent('com.psddev.cms.rte.clear')
      rteParent?.dispatchEvent(clearRte)

      // Move the focus to the correct element.
      const focusedRef = conversation.dataset.focusedRef
      if (focusedRef === undefined) {
        // Move focus to the edit control of the last post posted.

        const conversationEdit = conversation.querySelector<HTMLElement>(
          ':scope .Conversation-post:last-child .Conversation-edit',
        )

        if (conversationEdit) {
          conversationEdit.focus()
        }

        return
      }

      // Move focus to the reference post's edit control
      if (focusedRef !== 'null') {
        const conversationEdit = document
          .getElementById(focusedRef)
          ?.querySelector<HTMLAnchorElement>(':scope .Conversation-edit')

        if (conversationEdit) {
          conversationEdit.focus()
        }
      } else {
        const proseMirror = rteParent?.querySelector<HTMLElement>(
          ':scope .ProseMirror',
        )

        if (proseMirror) {
          proseMirror.focus()
        }
      }

      delete conversation.dataset.focusedRef
    }
  })
})

onFind(
  '.Conversation-newPost > .CIG-small > textarea[name="text"]',
  (input: HTMLTextAreaElement) => {
    input.addEventListener('input', async (event) => {
      const headers = new Headers()
      const cookie = Cookies.get('bsp.csrf')

      if (cookie) {
        headers.append('Brightspot-CSRF', cookie)
      }

      const body = new FormData()
      const newPostText = input.value
      const conversation = input.closest<HTMLElement>('.Conversation')

      if (newPostText) {
        body.append('postText', newPostText)
      }

      if (conversation) {
        const contentId = conversation.dataset.contentId
        const overlayId = conversation.dataset.overlayId
        if (contentId) body.append('objectId', contentId)
        if (overlayId) body.append('overlayId', overlayId)
        body.append('parentId', conversation.dataset.parentId || '')
        body.append('fieldName', conversation.dataset.fieldName || '')
        body.append('contextual', conversation.dataset.contextual || 'false')
      }

      await fetch(window.CONTEXT_PATH + 'conversation/wip-post', {
        method: 'post',
        credentials: 'include',
        headers,
        body,
      })
    })
  },
)

// Create Tether for Widget-actions in conversation-edit-post popup.
// This selector targets the Widget-actions element directly so it triggers
// each time the frame content is refreshed (unlike targeting the frame element
// which only triggers once due to the onFind marker attribute).
onFind(
  '.Popup[name^="conversation-edit-post"] .Widget-actions',
  (btn: HTMLElement) => {
    const popup = btn.closest<HTMLElement>('.Popup')
    if (popup) {
      new Tether(popup, btn)
    }
  },
)

onFind('.frame[name^=conversation-edit-post].loaded', (widget: HTMLElement) => {
  const popup = widget.closest<HTMLElement>('.Popup')

  const popupOpener: HTMLElement = popup
    ? ($(popup) as any).popup('source')[0]
    : null

  onFind(
    widget,
    ':scope button[name="action-delete"]',
    (button: HTMLButtonElement) => {
      button.addEventListener('click', () => {
        if (popupOpener instanceof HTMLElement) {
          const post = popupOpener.closest<HTMLElement>('.Conversation-post')
          // User deleted the post so look for a sibling post in the list
          // and store state that it should receive focus.
          if (post) {
            const nextPostId = post.nextElementSibling?.getAttribute('id')
            const container = post.closest<HTMLElement>('.Conversation')
            if (container && nextPostId) {
              container.dataset.focusedRef = nextPostId
            }
          }
        }
      })
    },
  )
})

onFind('.Conversation-add-comment', (button) => {
  button.addEventListener('click', (event) => {
    const workflowBody = button.closest('.ContentEdit-workflowBody')
    if (workflowBody) {
      workflowBody
        .getElementsByClassName('Conversation-workflow')[0]
        .setAttribute('style', 'display:block')
      button.setAttribute('style', 'display:none !important')
    }
  })
})

// When someone is added to watchers by commenting update ViewWatchersContentEditAction to reflect current count
onFind('.Conversation-posts[data-watchers-count]', (conversation) => {
  const newCount = conversation.getAttribute('data-watchers-count')
  const currentCount = document.querySelector('span.ViewWatchers-count')

  if (newCount && currentCount) {
    currentCount.textContent = newCount
  }
})

onFind(
  '.Popup[name="Contextual-comments"] .Conversation-pin, .Popup[name="Contextual-comments"] .Conversation-delete',
  (item: HTMLAnchorElement) => {
    item.href = item.href.replace('contextual=false', 'contextual=true')
    item.target = 'Contextual-comments'
  },
)

// Handle deleting posts from the edit post popup
onFind(
  '.Popup[name="conversation-edit-post"] .icon-action-delete',
  (post: HTMLAnchorElement) => {
    post.addEventListener('click', async () => {
      const headers = new Headers()
      const cookie = Cookies.get('bsp.csrf')

      if (cookie) {
        headers.append('Brightspot-CSRF', cookie)
      }

      const body = new FormData()

      const contentId = post.dataset.contentId
      const postId = post.dataset.postId
      const overlayId = post.dataset.overlayId

      if (contentId) body.append('objectId', contentId)
      if (postId) body.append('postId', postId)
      if (overlayId) body.append('overlayId', overlayId)

      await fetch(window.CONTEXT_PATH + 'conversation/delete-post', {
        method: 'post',
        credentials: 'include',
        headers,
        body,
      })

      const popup = post.closest('.Popup')

      if (popup) {
        ;($(popup) as any).popup('close')
      }
    })
  },
)

onFind('.Conversation-pinned-container', (container) => {
  if (!(container instanceof HTMLElement)) return
  const pinnedHeader = find(container, 'h1')
  if (pinnedHeader)
    pinnedHeader.addEventListener('click', (event) => {
      container.classList.toggle('is-expanded')
    })
})

onFind('.Conversation-actionsToggle', (more: HTMLElement) => {
  const list = more.nextElementSibling
  setupMenuForAria(list, more)
  more.addEventListener('click', (event) => {
    event.preventDefault()
    toggleMenu(list, more)
  })
})

// Depth-First Search (DFS) Algorithm that collects the ancestor path to a
// target value in a nested object.
function findPathByValue(
  obj: any,
  target: string,
  path: string[] = [],
): string[] {
  if (typeof obj !== 'object' || obj === null) {
    return []
  }

  // Iterate through the keys (and indices for arrays) of the current object
  for (const key in obj) {
    if (!Object.prototype.hasOwnProperty.call(obj, key)) continue
    const newPath = path.length > 0 ? [...path, key] : [key]
    const value = obj[key]

    // Check if the current value is the target
    if (value === target) {
      return newPath
    }

    // If the value is an object or array, recurse
    if (typeof value === 'object' && value !== null) {
      const foundPath = findPathByValue(value, target, newPath)
      if (foundPath.length > 0) {
        return foundPath
      }
    }
  }

  return []
}

function findClosestCigContainer(root: HTMLElement): HTMLElement | null {
  let container = root.closest('btu-cig-cluster')
  if (!container) {
    container = root.closest('.CIG-row')
  }
  return container
}

function clickClosestTab(root: HTMLElement) {
  setTimeout(() => {
    if (root.dataset.tab) {
      let tabs = root.closest<HTMLElement>('.TabBar')
      if (!tabs) {
        let sibling = root.previousElementSibling
        while (sibling) {
          if (sibling.classList.contains('TabBar')) {
            tabs = sibling as HTMLElement
            break
          }
          sibling = sibling.previousElementSibling
        }
        if (tabs) {
          tabs
            .querySelector<HTMLButtonElement>(
              `button[data-tab="${root.dataset.tab}"]`,
            )
            ?.click()
          return
        }
      }
    }
    const container = findClosestCigContainer(root)
    if (container && container.dataset.tab) {
      if (container.nodeName.toLowerCase() === 'btu-cig-cluster') {
        container
          .querySelector<HTMLElement>(
            ':scope > .CIG-title > button[aria-expanded="false"]',
          )
          ?.click()
      }
      const cig = container.closest('.CIG')
      if (cig) {
        cig
          .querySelector<HTMLButtonElement>(
            `.TabBar button[data-tab="${container.dataset.tab}"]`,
          )
          ?.click()
      }
    }
  }, 100)
}

// Scroll the element into view and wait for the animation to complete
function scrollToElement(
  element: HTMLElement,
  block: 'start' | 'center' | 'end' | 'nearest' = 'start',
) {
  element.scrollIntoView({ behavior: 'smooth', block: block })
  return new Promise((resolve) => setTimeout(resolve, 350))
}

// When a user clicks on post select the right tab and scroll the element into view
onFind('.Conversation-post', (post: HTMLElement) => {
  const fieldName = post.dataset.fieldName
  const contentEdit = post.closest<HTMLElement>('.ContentEdit')
  if (!fieldName || !contentEdit) return

  post.addEventListener('click', async (evt) => {
    const target = evt.currentTarget as HTMLElement
    const fieldName = target.dataset.fieldName

    if (!fieldName) return

    const fieldPaths = fieldName.split('/')
    const normalizedFieldName =
      fieldPaths.length === 1
        ? `${contentEdit.dataset.contentId}/${fieldName}`
        : fieldName
    const fieldParentId =
      fieldPaths.length === 1 ? contentEdit.dataset.contentId : fieldPaths[0]
    const root =
      document.querySelector<HTMLElement>(`[data-state*="${fieldParentId}"], 
      [data-content-id="${fieldParentId}"], 
      [data-object-id="${fieldParentId}"]`)

    if (!root || !fieldParentId) return

    clickClosestTab(root)

    const state = root.dataset.state

    if (!state) {
      // Check if field is on the root object
      const field = root.querySelector<HTMLElement>(
        `[data-name="${normalizedFieldName}"]`,
      )

      if (field) {
        clickClosestTab(field)
        field.scrollIntoView({ behavior: 'smooth', block: 'center' })
        highlightRow(field)
      }
      return
    }

    const json = JSON.parse(state)
    const path = findPathByValue(json, fieldParentId)

    // Expand root if necessary.
    const rootTitle = root.querySelector<HTMLElement>(
      ':scope > [class*="-title"][aria-expanded="false"]',
    )
    if (rootTitle) {
      await scrollToElement(root)
      root
        .querySelector<HTMLElement>(
          ':scope > [class*="-title"][aria-expanded="false"]',
        )
        ?.click()
    }

    let last = json
    let currentRoot = root
    const promises = []
    for (const key of path) {
      const value = last[key]

      if (Array.isArray(value)) {
        clickClosestTab(currentRoot)
        currentRoot
          .querySelector<HTMLElement>(
            ':scope > [class*="-title"][aria-expanded="false"]',
          )
          ?.click()
      } else if (typeof value === 'object') {
        if (value._id) {
          promises.push(
            new Promise<HTMLElement | null>((resolve) => {
              const timeout = setTimeout(() => {
                resolve(
                  currentRoot.querySelector<HTMLElement>(
                    `[data-name="${normalizedFieldName}"]`,
                  ),
                )
              }, 1000) // Timeout to avoid hanging

              onFindOnce(
                `li[data-id="${value._id}"] > [class*="-title"][aria-expanded="false"]`,
                async () => {
                  const nextRoot = currentRoot.querySelector<HTMLElement>(
                    `[data-id="${value._id}"]`,
                  )
                  if (nextRoot) {
                    clickClosestTab(nextRoot)
                    nextRoot
                      .querySelector<HTMLElement>(
                        `:scope > [class*="-title"][aria-expanded="false"]`,
                      )
                      ?.click()
                    currentRoot = nextRoot
                    await scrollToElement(currentRoot)
                  }
                  clearTimeout(timeout)
                  resolve(nextRoot)
                },
              )
            }),
          )
        }
      } else if (key === '_id' && normalizedFieldName.startsWith(value)) {
        const field = currentRoot.querySelector<HTMLElement>(
          `[data-name="${normalizedFieldName}"]`,
        )
        if (field) {
          clickClosestTab(field)
          await scrollToElement(field)
          highlightRow(field)
        }
      }

      last = value
    }

    Promise.all(promises).then(async (elements) => {
      elements.forEach(async (el) => {
        if (el) {
          await scrollToElement(el)
        }
      })
      highlightRow(elements.pop())
    })
  })
})

// Highlight the row for brief moment to make it easier to find
function highlightRow(item: HTMLElement | null | undefined) {
  if (!item) return
  const row = item.classList.contains('CIG-row')
    ? item
    : item.closest<HTMLElement>('.CIG-row')
  if (!row) return
  row.classList.add('is-highlighted')
  setTimeout(() => {
    row.classList.remove('is-highlighted')
  }, 2800)
}

// Helper function to manage animation state with cleanup
function withAnimation<T>(
  element: HTMLElement,
  animationClass: string,
  asyncWork: () => Promise<T>,
): Promise<T> {
  element.classList.add(animationClass)

  return asyncWork().finally(() => {
    element.classList.remove(animationClass)
  })
}

onFind(
  ['.Conversation-resolve', '.Conversation-unresolve'],
  (button: HTMLAnchorElement) => {
    // Prevent the default link navigation on click
    button.addEventListener('click', async (event) => {
      event.preventDefault()
      event.stopImmediatePropagation()

      // Use requestAnimationFrame to wait for animation completion, then perform resolve action
      const startTime = performance.now()

      await withAnimation(button, 'is-animating', () => {
        return new Promise<void>((resolve) => {
          const waitForAnimation = (currentTime: number) => {
            const elapsed = currentTime - startTime

            if (elapsed >= TIMING.ANIMATION_DURATION) {
              resolve()
            } else {
              requestAnimationFrame(waitForAnimation)
            }
          }
          requestAnimationFrame(waitForAnimation)
        })
      })

      // Animation completed, perform the resolve action
      const headers = new Headers()
      const cookie = Cookies.get('bsp.csrf')
      if (cookie) {
        headers.append('Brightspot-CSRF', cookie)
      }

      try {
        await fetch(button.href, {
          method: 'POST',
          credentials: 'include',
          headers,
        })
      } catch (error) {
        console.error('Failed to resolve post:', error)
      }
    })
  },
)

onFind('.Conversation-resolveThread', (button: HTMLAnchorElement) => {
  button.addEventListener('click', (event) => {
    // Prevent the default link behavior since we want to control the timing
    event.preventDefault()
    event.stopImmediatePropagation()

    // First, trigger the resolve action via fetch
    const headers = new Headers()
    const cookie = Cookies.get('bsp.csrf')
    if (cookie) {
      headers.append('Brightspot-CSRF', cookie)
    }

    // Extract resolve URL and make the request
    fetch(button.href, {
      method: 'POST',
      credentials: 'include',
      headers,
    }).then(async () => {
      const popup = button.closest('.Popup')
      if (popup) {
        window.jQuery?.(popup).popup('close')
      }
    })
  })
})
