import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { StudioProvider, useStudio } from './studio/StudioContext'
import StudioDocumentPane from './studio/StudioDocumentPane'
import StudioGridPane from './studio/StudioGridPane'
import { clamp, findCell, getPages, getTablesForPage, updateAnnotationCell } from './studio/studioUtils'

import API from '../api'
const SPLIT_MIN = 0.2
const SPLIT_MAX = 0.8

async function parseJsonResponse(response) {
  let data = null
  try {
    data = await response.json()
  } catch {
    data = null
  }

  if (!response.ok) {
    const detail = data?.detail || `Request failed with status ${response.status}`
    throw new Error(detail)
  }

  return data || {}
}

function getToastPosition(clientX, clientY) {
  if (Number.isFinite(clientX) && Number.isFinite(clientY)) {
    return { x: clientX, y: clientY }
  }
  return { x: 160, y: 90 }
}

function cloneAnnotation(annotation) {
  return JSON.parse(JSON.stringify(annotation || {}))
}

function findTable(annotation, tableId) {
  return (annotation?.tables || []).find((table) => table.table_id === tableId) || null
}

function buildOccupancy(table) {
  const occupied = new Set()
  for (const cell of table?.cells || []) {
    const row = Math.max(0, Number(cell.row) || 0)
    const col = Math.max(0, Number(cell.col) || 0)
    const rowSpan = Math.max(1, Number(cell.row_span) || 1)
    const colSpan = Math.max(1, Number(cell.col_span) || 1)
    for (let r = row; r < row + rowSpan; r += 1) {
      for (let c = col; c < col + colSpan; c += 1) {
        occupied.add(`${r}:${c}`)
      }
    }
  }
  return occupied
}

function findNextEmptySlot(table, preferredRow = 0) {
  const occupied = buildOccupancy(table)
  const cells = table?.cells || []
  const rowBound = Math.max(1, ...cells.map((cell) => (Number(cell.row) || 0) + Math.max(1, Number(cell.row_span) || 1)))
  const colBound = Math.max(1, ...cells.map((cell) => (Number(cell.col) || 0) + Math.max(1, Number(cell.col_span) || 1)))

  for (let row = Math.max(0, preferredRow); row < rowBound; row += 1) {
    for (let col = 0; col < colBound + 2; col += 1) {
      if (!occupied.has(`${row}:${col}`)) {
        return { row, col }
      }
    }
  }

  return { row: rowBound, col: 0 }
}

async function copyTextToClipboard(text) {
  if (navigator.clipboard?.writeText) {
    await navigator.clipboard.writeText(text)
    return
  }

  const textarea = document.createElement('textarea')
  textarea.value = text
  textarea.setAttribute('readonly', 'true')
  textarea.style.position = 'fixed'
  textarea.style.opacity = '0'
  document.body.appendChild(textarea)
  textarea.select()
  document.execCommand('copy')
  document.body.removeChild(textarea)
}

function StudioWorkspace({ job, onBack, onJobUpdate }) {
  const { state, dispatch } = useStudio()
  const [annotation, setAnnotation] = useState(() => cloneAnnotation(job.annotation))
  const [isEditingGridCell, setIsEditingGridCell] = useState(false)
  const [hoveredCell, setHoveredCell] = useState(null)
  const [splitRatio, setSplitRatio] = useState(0.5)
  const annotationRef = useRef(annotation)
  const workspaceRef = useRef(null)
  const pages = useMemo(() => getPages(annotation), [annotation])
  const pageCount = pages.length
  const activePageTables = useMemo(
    () => getTablesForPage(annotation, state.activePage),
    [annotation, state.activePage],
  )

  const showToast = useCallback((message, clientX, clientY) => {
    const position = getToastPosition(clientX, clientY)
    dispatch({
      type: 'showToast',
      toast: {
        message,
        x: position.x,
        y: position.y,
      },
    })
  }, [dispatch])

  useEffect(() => {
    annotationRef.current = annotation
  }, [annotation])

  useEffect(() => {
    const tableOnPage = activePageTables.find((table) => table.table_id === state.activeTable)
    if (!tableOnPage && activePageTables.length) {
      dispatch({ type: 'setActiveTable', tableId: activePageTables[0].table_id })
    }
    if (!activePageTables.length && state.activeTable !== null) {
      dispatch({ type: 'setActiveTable', tableId: null })
    }
  }, [activePageTables, dispatch, state.activeTable])

  useEffect(() => {
    if (!state.activeCell) {
      return
    }
    const current = findCell(annotation, state.activeCell)
    if (!current || (current.table.page || 0) !== state.activePage) {
      dispatch({ type: 'setActiveCell', cell: null })
    }
  }, [annotation, dispatch, state.activeCell, state.activePage])

  useEffect(() => {
    if (!state.toast) {
      return undefined
    }
    const timeout = window.setTimeout(() => {
      dispatch({ type: 'clearToast' })
    }, 1500)
    return () => window.clearTimeout(timeout)
  }, [dispatch, state.toast])

  const handleAnnotationUpdate = useCallback((nextAnnotation) => {
    annotationRef.current = nextAnnotation
    setAnnotation(nextAnnotation)
    onJobUpdate?.(job.id, { annotation: nextAnnotation })
  }, [job.id, onJobUpdate])

  const handleCopyText = useCallback(async (text, clientX, clientY) => {
    try {
      await copyTextToClipboard(text)
      showToast('Copied!', clientX, clientY)
    } catch (error) {
      console.error('Copy failed', error)
      dispatch({ type: 'incrementErrors' })
      showToast('Copy failed', clientX, clientY)
    }
  }, [dispatch, showToast])

  const commitCellEdit = useCallback(async (target, text, { historyMode }) => {
    const match = findCell(annotationRef.current, target)
    if (!match) {
      dispatch({ type: 'incrementErrors' })
      return false
    }

    const previousText = String(match.cell.text || '')
    const nextText = String(text || '')
    if (previousText === nextText) {
      return true
    }

    try {
      const response = await fetch(`${API}/api/results/${job.id}/cells`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          table_id: target.tableId,
          row: target.row,
          col: target.col,
          text: nextText,
        }),
      })

      if (!response.ok) {
        throw new Error(`Save failed with status ${response.status}`)
      }

      const nextAnnotation = updateAnnotationCell(annotationRef.current, target, nextText)
      handleAnnotationUpdate(nextAnnotation)
      dispatch({ type: 'setActiveTable', tableId: target.tableId })
      dispatch({ type: 'setActiveCell', cell: target })

      if (historyMode === 'record') {
        dispatch({
          type: 'recordEdit',
          edit: {
            ...target,
            previousText,
            nextText,
          },
        })
      }

      return true
    } catch (error) {
      console.error('Cell edit failed', error)
      dispatch({ type: 'incrementErrors' })
      return false
    }
  }, [dispatch, handleAnnotationUpdate, job.id])

  const handleUndo = useCallback(async () => {
    const edit = state.undoStack[state.undoStack.length - 1]
    if (!edit) {
      return
    }
    const didSave = await commitCellEdit(
      { tableId: edit.tableId, row: edit.row, col: edit.col },
      edit.previousText,
      { historyMode: 'undo' },
    )
    if (didSave) {
      dispatch({ type: 'completeUndo' })
    }
  }, [commitCellEdit, dispatch, state.undoStack])

  const handleRedo = useCallback(async () => {
    const edit = state.redoStack[state.redoStack.length - 1]
    if (!edit) {
      return
    }
    const didSave = await commitCellEdit(
      { tableId: edit.tableId, row: edit.row, col: edit.col },
      edit.nextText,
      { historyMode: 'redo' },
    )
    if (didSave) {
      dispatch({ type: 'completeRedo' })
    }
  }, [commitCellEdit, dispatch, state.redoStack])

  useEffect(() => {
    const handleKeyDown = (event) => {
      const modifier = event.metaKey || event.ctrlKey
      const tag = document.activeElement?.tagName
      const isEditable = document.activeElement?.isContentEditable || tag === 'TEXTAREA' || tag === 'INPUT'
      if (!modifier || isEditable || isEditingGridCell) {
        return
      }
      const key = event.key.toLowerCase()
      if (key !== 'z') {
        return
      }
      event.preventDefault()
      if (event.shiftKey) {
        void handleRedo()
      } else {
        void handleUndo()
      }
    }

    window.addEventListener('keydown', handleKeyDown)
    return () => window.removeEventListener('keydown', handleKeyDown)
  }, [handleRedo, handleUndo, isEditingGridCell])

  const handleMarqueeOCR = useCallback(async ({ bbox, clientX, clientY }) => {
    try {
      const response = await fetch(`${API}/api/process/${job.id}/ocr`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          page_index: state.activePage,
          bbox: bbox,
        }),
      })
      const data = await parseJsonResponse(response)
      if (!data.ok) {
        throw new Error('OCR request failed')
      }

      const text = String(data.text || '').trim()
      if (!text) {
        showToast('No text detected in selection', clientX, clientY)
        return
      }

      // Auto-copy to clipboard — no blocking confirm dialog.
      await copyTextToClipboard(text)
      showToast(`OCR copied: "${text.slice(0, 40)}${text.length > 40 ? '…' : ''}"`, clientX, clientY)
    } catch (error) {
      console.error('Marquee OCR failed', error)
      dispatch({ type: 'incrementErrors' })
      showToast('Marquee OCR failed', clientX, clientY)
    } finally {
      dispatch({ type: 'setSelection', selection: null })
      dispatch({ type: 'setTool', tool: 'pointer' })
    }
  }, [
    job.id,
    state.activePage,
    dispatch,
    showToast,
  ])

  const handleTableSelection = useCallback(async ({ bbox, clientX, clientY }) => {
    try {
      const response = await fetch(`${API}/api/process/${job.id}/tsr`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          page_index: state.activePage,
          bbox: bbox,
        }),
      })
      const data = await parseJsonResponse(response)
      if (!data.ok) {
        throw new Error('TSR request failed')
      }

      if (data.annotation) {
        handleAnnotationUpdate(data.annotation)
      }

      const table = data.table
      if (table) {
        dispatch({ type: 'setActiveTable', tableId: table.table_id })
        dispatch({ type: 'setActivePage', page: table.page || state.activePage })
      }
      showToast('Table structure added', clientX, clientY)
    } catch (error) {
      console.error('TSR failed', error)
      dispatch({ type: 'incrementErrors' })
      showToast('Table structure extraction failed', clientX, clientY)
    } finally {
      dispatch({ type: 'setSelection', selection: null })
      dispatch({ type: 'setTool', tool: 'pointer' })
    }
  }, [
    job.id,
    state.activePage,
    dispatch,
    handleAnnotationUpdate,
    showToast,
  ])

  const requestDeleteTable = useCallback(async (tableId) => {
    const response = await fetch(`${API}/api/results/${job.id}/tables/${tableId}/delete`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
    })
    return parseJsonResponse(response)
  }, [job.id])

  const handleReprocessTable = useCallback(async ({ tableId, pageIndex, bbox, clientX, clientY }) => {
    try {
      const response = await fetch(`${API}/api/process/${job.id}/tsr`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          page_index: pageIndex,
          bbox,
        }),
      })
      const data = await parseJsonResponse(response)
      if (!data.ok || !data.annotation || !data.table) {
        throw new Error('TSR request failed')
      }

      let nextAnnotation = data.annotation
      const nextTableId = Number(data.table.table_id)

      // Keep a single table for that user intent: replace selected table with reprocessed result.
      if (Number.isFinite(tableId) && tableId !== nextTableId) {
        try {
          const deleteData = await requestDeleteTable(tableId)
          if (deleteData.ok && deleteData.annotation) {
            nextAnnotation = deleteData.annotation
          }
        } catch (deleteError) {
          console.error('Failed to delete previous table after TSR rerun', deleteError)
          dispatch({ type: 'incrementErrors' })
          showToast('TSR finished, but old table could not be removed', clientX, clientY)
        }
      }

      handleAnnotationUpdate(nextAnnotation)
      dispatch({ type: 'setActivePage', page: pageIndex })
      dispatch({ type: 'setActiveTable', tableId: nextTableId })
      showToast('Table reprocessed with TSR + OCR', clientX, clientY)
      return true
    } catch (error) {
      console.error('Table reprocess failed', error)
      dispatch({ type: 'incrementErrors' })
      showToast(error.message || 'Table reprocess failed', clientX, clientY)
      return false
    }
  }, [dispatch, handleAnnotationUpdate, job.id, requestDeleteTable, showToast])

  const handleDeleteTable = useCallback(async ({ tableId, clientX, clientY }) => {
    try {
      const data = await requestDeleteTable(tableId)
      if (!data.ok || !data.annotation) {
        throw new Error('Delete table failed')
      }
      handleAnnotationUpdate(data.annotation)
      setHoveredCell((current) => (current?.tableId === tableId ? null : current))
      dispatch({ type: 'setActiveCell', cell: null })
      showToast('Table deleted', clientX, clientY)
      return true
    } catch (error) {
      console.error('Delete table failed', error)
      dispatch({ type: 'incrementErrors' })
      showToast(error.message || 'Delete table failed', clientX, clientY)
      return false
    }
  }, [dispatch, handleAnnotationUpdate, requestDeleteTable, showToast])

  const handleReplaceTable = useCallback(async ({ tableId, cells }) => {
    try {
      const response = await fetch(`${API}/api/results/${job.id}/tables/${tableId}/cells`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          table_id: tableId,
          cells,
        }),
      })
      const data = await parseJsonResponse(response)
      if (!data.ok || !data.annotation) {
        throw new Error('Table update failed')
      }
      handleAnnotationUpdate(data.annotation)
      dispatch({ type: 'setActiveTable', tableId })
      return true
    } catch (error) {
      console.error('Table replacement failed', error)
      dispatch({ type: 'incrementErrors' })
      showToast(error.message || 'Table update failed')
      return false
    }
  }, [dispatch, handleAnnotationUpdate, job.id, showToast])

  const handleAddCell = useCallback(async ({ tableId }) => {
    const table = findTable(annotationRef.current, tableId)
    if (!table) {
      showToast('Select a table first')
      return false
    }

    const preferredRow = state.activeCell?.tableId === tableId ? state.activeCell.row : 0
    const slot = findNextEmptySlot(table, preferredRow)

    try {
      const response = await fetch(`${API}/api/results/${job.id}/cells/add`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          table_id: tableId,
          row: slot.row,
          col: slot.col,
          row_span: 1,
          col_span: 1,
          text: '',
        }),
      })
      const data = await parseJsonResponse(response)
      if (!data.ok || !data.annotation) {
        throw new Error('Add cell failed')
      }
      handleAnnotationUpdate(data.annotation)
      dispatch({ type: 'setActiveTable', tableId })
      dispatch({ type: 'setActiveCell', cell: { tableId, row: slot.row, col: slot.col } })
      showToast(`Cell added at r${slot.row + 1} c${slot.col + 1}`)
      return true
    } catch (error) {
      console.error('Add cell failed', error)
      dispatch({ type: 'incrementErrors' })
      showToast(error.message || 'Add cell failed')
      return false
    }
  }, [dispatch, handleAnnotationUpdate, job.id, showToast, state.activeCell])

  const handleDeleteCell = useCallback(async ({ tableId, row, col }) => {
    try {
      const response = await fetch(`${API}/api/results/${job.id}/cells/delete`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          table_id: tableId,
          row,
          col,
        }),
      })
      const data = await parseJsonResponse(response)
      if (!data.ok || !data.annotation) {
        throw new Error('Delete cell failed')
      }
      handleAnnotationUpdate(data.annotation)
      if (state.activeCell && state.activeCell.tableId === tableId && state.activeCell.row === row && state.activeCell.col === col) {
        dispatch({ type: 'setActiveCell', cell: null })
      }
      showToast(`Cell deleted from r${row + 1} c${col + 1}`)
      return true
    } catch (error) {
      console.error('Delete cell failed', error)
      dispatch({ type: 'incrementErrors' })
      showToast(error.message || 'Delete cell failed')
      return false
    }
  }, [dispatch, handleAnnotationUpdate, job.id, showToast, state.activeCell])

  const handleStartResize = useCallback((event) => {
    event.preventDefault()
    const workspace = workspaceRef.current
    if (!workspace) {
      return
    }

    const updateSplit = (clientX) => {
      const bounds = workspace.getBoundingClientRect()
      if (!bounds.width) {
        return
      }
      const ratio = (clientX - bounds.left) / bounds.width
      setSplitRatio(clamp(ratio, SPLIT_MIN, SPLIT_MAX))
    }

    updateSplit(event.clientX)
    const handleMove = (moveEvent) => {
      updateSplit(moveEvent.clientX)
    }

    const handleUp = () => {
      window.removeEventListener('pointermove', handleMove)
      window.removeEventListener('pointerup', handleUp)
      window.removeEventListener('pointercancel', handleUp)
    }

    window.addEventListener('pointermove', handleMove)
    window.addEventListener('pointerup', handleUp)
    window.addEventListener('pointercancel', handleUp)
  }, [])

  const handleExport = useCallback((format) => {
    window.open(`${API}/api/export/${job.id}?format=${format}`, '_blank', 'noopener,noreferrer')
  }, [job.id])

  const secondsPerPage = pageCount ? Number(job.duration || 0) / pageCount : 0
  return (
    <div className="studio-shell">
      <header className="studio-header">
        <div className="studio-header-brand">
          <button type="button" className="studio-brand-button" onClick={onBack}>
            Agent P-DF
          </button>
          <div className="studio-header-file">{job.filename}</div>
        </div>

        <div className="studio-header-pill">
          <span>Pages: {Math.min(state.activePage + 1, pageCount)}/{pageCount}</span>
          <span>Speed: {secondsPerPage.toFixed(1)} sec/page</span>
          <span>Errors: {state.errorCount}</span>
        </div>

        <div className="studio-header-actions">
          <button type="button" className="btn" onClick={onBack}>
            Back / New Upload
          </button>
          <button type="button" className="btn btn-primary btn-export" onClick={() => handleExport('csv')}>
            Export CSV
          </button>
          <button type="button" className="btn btn-primary btn-export" onClick={() => handleExport('xlsx')}>
            Export Excel
          </button>
          <button type="button" className="btn btn-primary btn-export" onClick={() => handleExport('html')}>
            Export HTML
          </button>
        </div>
      </header>

      <div className="studio-workspace" ref={workspaceRef}>
        <div className="studio-pane-wrap" style={{ flexBasis: `${splitRatio * 100}%` }}>
          <StudioDocumentPane
            annotation={annotation}
            jobId={job.id}
            onCopyText={handleCopyText}
            hoveredCell={hoveredCell}
            onHoverCellChange={setHoveredCell}
            onReprocessTable={handleReprocessTable}
            onDeleteTable={handleDeleteTable}
            onAddCell={handleAddCell}
            onDeleteCell={handleDeleteCell}
            onSelectionEnd={(selectionPayload) => {
               if (state.activeTool === 'marquee') handleMarqueeOCR(selectionPayload)
               else handleTableSelection(selectionPayload)
              }}
          />
        </div>

        <div
          className="studio-splitter"
          role="separator"
          aria-orientation="vertical"
          aria-valuemin={Math.round(SPLIT_MIN * 100)}
          aria-valuemax={Math.round(SPLIT_MAX * 100)}
          aria-valuenow={Math.round(splitRatio * 100)}
          onPointerDown={handleStartResize}
          onDoubleClick={() => setSplitRatio(0.5)}
        />

        <div className="studio-pane-wrap studio-pane-wrap-right" style={{ flexBasis: `${(1 - splitRatio) * 100}%` }}>
          <StudioGridPane
            annotation={annotation}
            onEditingChange={setIsEditingGridCell}
            onReplaceTable={handleReplaceTable}
            hoveredCell={hoveredCell}
            onHoverCellChange={setHoveredCell}
          />
        </div>
      </div>

      {state.toast && (
        <div
          className="studio-toast"
          style={{
            left: state.toast.x,
            top: state.toast.y,
          }}
        >
          {state.toast.message}
        </div>
      )}
    </div>
  )
}

export default function ResultsView({ job, onBack, onJobUpdate }) {
  return (
    <StudioProvider key={job.id} annotation={job.annotation}>
      <StudioWorkspace job={job} onBack={onBack} onJobUpdate={onJobUpdate} />
    </StudioProvider>
  )
}
