import {
  Button,
  CloseTrigger,
  DotIndicator,
  Icon,
  Input,
  Label,
  Modal,
  Switch,
  Tabs,
  Text,
  Tooltip,
  toast,
  useValueWithDefault,
} from "@repo/ui"
import { eq } from "@tanstack/react-db"
import { createFileRoute, useParams } from "@tanstack/react-router"
import { useProjectFlaggers } from "../../../../../domains/flaggers/flaggers.collection.ts"
import { useProjectsCollection } from "../../../../../domains/projects/projects.collection.ts"
import { BreadcrumbText } from "../../../-components/breadcrumb-ui.tsx"

function IssuesBreadcrumb() {
  const { projectSlug } = useParams({ strict: false })
  const { data: project } = useProjectsCollection(
    (projects) => projects.where(({ project: p }) => eq(p.slug, projectSlug ?? "")).findOne(),
    [projectSlug],
  )
  const { data: flaggers = [] } = useProjectFlaggers(project?.id ?? "")
  const hasActiveFlaggers = flaggers.some((f) => f.enabled)

  return (
    <span className="flex items-center gap-0">
      <BreadcrumbText variant="current">Issues</BreadcrumbText>
      {hasActiveFlaggers && (
        <Tooltip
          side="bottom"
          align="center"
          trigger={
            <span className="flex h-5 w-5 items-center justify-center cursor-default">
              <DotIndicator variant="primary" size="md" ping />
            </span>
          }
        >
          Latitude is always scanning for common issues
        </Tooltip>
      )}
    </span>
  )
}

import { ActivityIcon, ArchiveIcon, CheckIcon, DownloadIcon, PauseIcon, SearchIcon } from "lucide-react"
import { useCallback, useMemo, useRef, useState } from "react"
import { useDebounce } from "react-use"
import { invalidateIssueQueries, useIssues } from "../../../../../domains/issues/issues.collection.ts"
import {
  applyBulkIssueLifecycleAction,
  enqueueIssuesExport,
  type IssueRecord,
} from "../../../../../domains/issues/issues.functions.ts"
import { ListingLayout as Layout } from "../../../../../layouts/ListingLayout/index.tsx"
import { toUserMessage } from "../../../../../lib/errors.ts"
import { useParamState } from "../../../../../lib/hooks/useParamState.ts"
import { EMPTY_SELECTION, type SelectionState, useSelectableRows } from "../../../../../lib/hooks/useSelectableRows.ts"
import { ColumnsSelector } from "../-components/columns-selector.tsx"
import { ExportConfirmationModal } from "../-components/export-confirmation-modal.tsx"
import { useTableColumnSettings } from "../-components/table-column-settings.ts"
import { TimeFilterDropdown } from "../-components/time-filter-dropdown.tsx"
import { useRouteProject } from "../-route-data.ts"
import { IssueDetailDrawer } from "./-components/issue-detail-drawer.tsx"
import { IssuesAnalyticsPanel } from "./-components/issues-analytics-panel.tsx"
import { IssuesEmptyState } from "./-components/issues-empty-state.tsx"
import {
  ISSUES_COLUMN_OPTIONS,
  type IssuesColumnId,
  type IssuesTableSorting,
  IssuesView,
} from "./-components/issues-view.tsx"

const DEFAULT_SORTING: IssuesTableSorting = { column: "lastSeen", direction: "desc" }
const ISSUE_SEARCH_DEBOUNCE_MS = 300
const SORT_COLUMNS = ["lastSeen", "occurrences", "state"] as const satisfies readonly IssuesTableSorting["column"][]
const SORT_DIRECTIONS = ["asc", "desc"] as const satisfies readonly IssuesTableSorting["direction"][]
const SORT_PARAM_PATTERN = /^(lastSeen|occurrences|state):(asc|desc)$/
const EMPTY_ISSUES: readonly IssueRecord[] = []

function serializeSorting(sorting: IssuesTableSorting): string {
  return `${sorting.column}:${sorting.direction}`
}

function parseSorting(raw: string): IssuesTableSorting {
  const [column, direction] = raw.split(":")
  if (
    SORT_COLUMNS.includes(column as IssuesTableSorting["column"]) &&
    SORT_DIRECTIONS.includes(direction as IssuesTableSorting["direction"])
  ) {
    return {
      column: column as IssuesTableSorting["column"],
      direction: direction as IssuesTableSorting["direction"],
    }
  }
  return DEFAULT_SORTING
}

export const Route = createFileRoute("/_authenticated/projects/$projectSlug/issues/")({
  staticData: {
    breadcrumb: IssuesBreadcrumb,
  },
  component: IssuesPage,
})

function IssuesPage() {
  const project = useRouteProject()
  const [activeIssueId, setActiveIssueId] = useParamState("issueId", "")
  const [lifecycleGroup, setLifecycleGroup] = useParamState("issuesLifecycle", "active", {
    validate: (value): value is "active" | "archived" => value === "active" || value === "archived",
  })
  const [timeFrom, setTimeFrom] = useParamState("issuesTimeFrom", "")
  const [timeTo, setTimeTo] = useParamState("issuesTimeTo", "")
  const [searchQuery, setSearchQuery] = useParamState("issuesSearch", "")
  const [searchInput, setSearchInput] = useValueWithDefault(searchQuery)
  const [rawSorting, setRawSorting] = useParamState("issuesSort", serializeSorting(DEFAULT_SORTING), {
    validate: (value): value is string => SORT_PARAM_PATTERN.test(value),
  })
  const sorting = useMemo(() => parseSorting(rawSorting), [rawSorting])
  const setSorting = useCallback((next: IssuesTableSorting) => setRawSorting(serializeSorting(next)), [setRawSorting])
  const [selectionState, setSelectionState] = useState<SelectionState<string>>(EMPTY_SELECTION)
  const [exportModalOpen, setExportModalOpen] = useState(false)
  const [exporting, setExporting] = useState(false)
  const [bulkResolveModalOpen, setBulkResolveModalOpen] = useState(false)
  const [bulkIgnoreModalOpen, setBulkIgnoreModalOpen] = useState(false)
  const [keepMonitoring, setKeepMonitoring] = useState(true)
  const [bulkActionLoading, setBulkActionLoading] = useState(false)
  const issueIdsRef = useRef<string[]>([])

  useDebounce(
    () => {
      const normalizedSearchQuery = searchInput.trim()
      if (normalizedSearchQuery !== searchQuery) {
        setSearchQuery(normalizedSearchQuery)
      }
    },
    ISSUE_SEARCH_DEBOUNCE_MS,
    [searchInput, searchQuery, setSearchQuery],
  )

  const columnSettings = useTableColumnSettings<IssuesColumnId>({
    storageKey: "projects.issues.columns.v1",
    columns: ISSUES_COLUMN_OPTIONS,
  })
  const timeRange =
    timeFrom || timeTo
      ? {
          ...(timeFrom ? { fromIso: timeFrom } : {}),
          ...(timeTo ? { toIso: timeTo } : {}),
        }
      : undefined

  const {
    data: issuesData,
    analytics,
    occurrencesSum,
    totalCount,
    isLoading,
    isReloading,
    infiniteScroll,
  } = useIssues({
    projectId: project.id,
    lifecycleGroup,
    sorting,
    ...(searchQuery ? { searchQuery } : {}),
    ...(timeRange ? { timeRange } : {}),
  })

  // While a filter/sort change is in flight we hide the (now stale) previous
  // rows so the table and analytics fall back to their skeleton state. The
  // surrounding page layout (filters, search, lifecycle tabs) keeps rendering
  // because `isLoading` itself stays false during the placeholder window.
  const showSkeletons = isLoading || isReloading
  const issues = isReloading ? EMPTY_ISSUES : issuesData

  const currentIssueIndex = activeIssueId ? issueIdsRef.current.indexOf(activeIssueId) : -1
  const issueIds = useMemo(() => issues.map((issue) => issue.id), [issues])
  const selection = useSelectableRows({
    rowIds: issueIds,
    totalRowCount: totalCount,
    controlledState: selectionState,
    onStateChange: setSelectionState,
  })
  const canNavigateNext =
    issueIdsRef.current.length > 0 && (currentIssueIndex < 0 || currentIssueIndex < issueIdsRef.current.length - 1)
  const canNavigatePrev = issueIdsRef.current.length > 0 && (currentIssueIndex < 0 || currentIssueIndex > 0)

  const handleExportIssues = useCallback(async () => {
    const bulkSelection = selection.bulkSelection
    if (!bulkSelection) return

    setExporting(true)
    try {
      await enqueueIssuesExport({
        data: {
          projectId: project.id,
          selection: bulkSelection,
          lifecycleGroup,
          sort: {
            field: sorting.column,
            direction: sorting.direction,
          },
          ...(searchQuery ? { searchQuery } : {}),
          ...(timeRange ? { timeRange } : {}),
        },
      })
      toast({
        title: "Export started",
        description: "You'll receive an email with a download link when your export is ready.",
      })
      selection.clearSelections()
      setExportModalOpen(false)
    } catch (error) {
      toast({
        variant: "destructive",
        description: error instanceof Error ? error.message : "Export failed",
      })
    } finally {
      setExporting(false)
    }
  }, [lifecycleGroup, project.id, searchQuery, selection, sorting.column, sorting.direction, timeRange])

  const handleBulkResolve = useCallback(async () => {
    const bulkSelection = selection.bulkSelection
    if (!bulkSelection) return

    setBulkActionLoading(true)
    try {
      const result = await applyBulkIssueLifecycleAction({
        data: {
          projectId: project.id,
          selection: bulkSelection,
          command: "resolve",
          keepMonitoring,
          lifecycleGroup,
          sort: {
            field: sorting.column,
            direction: sorting.direction,
          },
          ...(searchQuery ? { searchQuery } : {}),
          ...(timeRange ? { timeRange } : {}),
        },
      })
      const changedCount = result.items.filter((item) => item.changed).length
      await invalidateIssueQueries(project.id)
      toast({
        description:
          changedCount === 0
            ? "No issues were resolved."
            : changedCount === 1
              ? "1 issue resolved."
              : `${changedCount} issues resolved.`,
      })
      selection.clearSelections()
      setBulkResolveModalOpen(false)
    } catch (error) {
      toast({
        variant: "destructive",
        description: toUserMessage(error),
      })
    } finally {
      setBulkActionLoading(false)
    }
  }, [keepMonitoring, lifecycleGroup, project.id, searchQuery, selection, sorting.column, sorting.direction, timeRange])

  const handleBulkIgnore = useCallback(async () => {
    const bulkSelection = selection.bulkSelection
    if (!bulkSelection) return

    setBulkActionLoading(true)
    try {
      const result = await applyBulkIssueLifecycleAction({
        data: {
          projectId: project.id,
          selection: bulkSelection,
          command: "ignore",
          lifecycleGroup,
          sort: {
            field: sorting.column,
            direction: sorting.direction,
          },
          ...(searchQuery ? { searchQuery } : {}),
          ...(timeRange ? { timeRange } : {}),
        },
      })
      const changedCount = result.items.filter((item) => item.changed).length
      await invalidateIssueQueries(project.id)
      toast({
        description:
          changedCount === 0
            ? "No issues were ignored."
            : changedCount === 1
              ? "1 issue ignored."
              : `${changedCount} issues ignored.`,
      })
      selection.clearSelections()
      setBulkIgnoreModalOpen(false)
    } catch (error) {
      toast({
        variant: "destructive",
        description: toUserMessage(error),
      })
    } finally {
      setBulkActionLoading(false)
    }
  }, [lifecycleGroup, project.id, searchQuery, selection, sorting.column, sorting.direction, timeRange])

  const hasActiveFilters = lifecycleGroup !== "active" || searchQuery !== "" || Boolean(timeRange)
  // Derived from the un-substituted data so a placeholder reload (which forces
  // `issues` to []) does not falsely trigger the empty state.
  const hasNoIssues = issuesData.length === 0 && !hasActiveFilters
  const showEmptyState = !showSkeletons && hasNoIssues

  if (isLoading && hasNoIssues) {
    return (
      <Layout>
        <Layout.Content>
          <IssuesEmptyState isLoading />
        </Layout.Content>
      </Layout>
    )
  }

  if (showEmptyState) {
    return (
      <Layout>
        <Layout.Content>
          <IssuesEmptyState />
        </Layout.Content>
      </Layout>
    )
  }

  return (
    <Layout>
      <Layout.Content>
        <Layout.Actions>
          <Layout.ActionsRow>
            <Layout.ActionRowItem>
              <TimeFilterDropdown
                {...(timeFrom ? { startTimeFrom: timeFrom } : {})}
                {...(timeTo ? { startTimeTo: timeTo } : {})}
                onChange={(from, to) => {
                  setTimeFrom(from ?? "")
                  setTimeTo(to ?? "")
                }}
              />
            </Layout.ActionRowItem>
            <Layout.ActionRowItem>
              <ColumnsSelector
                columns={columnSettings.columns}
                selectedColumnIds={columnSettings.visibleColumnIds}
                onChange={(nextColumnIds) => columnSettings.setVisibleColumnIds(nextColumnIds as IssuesColumnId[])}
                onOrderChange={(nextColumnIds) => columnSettings.setColumnIds(nextColumnIds as IssuesColumnId[])}
              />
              <div className="relative">
                <Input
                  value={searchInput}
                  onChange={(event) => setSearchInput(event.target.value)}
                  placeholder="Search issues"
                  size="sm"
                  className="w-64 pl-8 rounded-lg"
                />
                <SearchIcon className="pointer-events-none absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
              </div>
              <Tabs
                variant="bordered"
                size="sm"
                options={[
                  {
                    id: "active",
                    label: "Active",
                    icon: <ActivityIcon className="w-4 h-4" />,
                  },
                  {
                    id: "archived",
                    label: "Archived",
                    icon: <ArchiveIcon className="w-4 h-4" />,
                  },
                ]}
                active={lifecycleGroup}
                onSelect={(value) => setLifecycleGroup(value)}
              />
            </Layout.ActionRowItem>
          </Layout.ActionsRow>
        </Layout.Actions>
        {selection.selectedCount > 0 && (
          <div className="flex items-center gap-2 px-6">
            <Button
              variant="outline"
              size="sm"
              onClick={() => setBulkIgnoreModalOpen(true)}
              disabled={bulkActionLoading}
            >
              <Icon icon={PauseIcon} size="sm" />
              Ignore ({selection.selectedCount.toLocaleString()})
            </Button>
            <Button
              variant="outline"
              size="sm"
              onClick={() => {
                setKeepMonitoring(true)
                setBulkResolveModalOpen(true)
              }}
              disabled={bulkActionLoading}
            >
              <Icon icon={CheckIcon} size="sm" />
              Resolve ({selection.selectedCount.toLocaleString()})
            </Button>
            <Button variant="outline" size="sm" onClick={() => setExportModalOpen(true)} disabled={exporting}>
              <Icon icon={DownloadIcon} size="sm" />
              Export ({selection.selectedCount.toLocaleString()})
            </Button>
          </div>
        )}
        <div className="px-6">
          <IssuesAnalyticsPanel
            projectId={project.id}
            analytics={analytics}
            isLoading={showSkeletons}
            onRangeSelect={(range) => {
              setTimeFrom(range?.from ?? "")
              setTimeTo(range?.to ?? "")
            }}
          />
        </div>
        <IssuesView
          issues={issues}
          isLoading={showSkeletons}
          infiniteScroll={infiniteScroll}
          sorting={sorting}
          occurrencesSum={occurrencesSum}
          visibleColumnIds={columnSettings.visibleColumnIds}
          activeIssueId={activeIssueId || undefined}
          selection={selection}
          onSortChange={setSorting}
          onActiveIssueChange={(issueId) => setActiveIssueId(issueId ?? "")}
          issueIdsRef={issueIdsRef}
        />
        {selection.bulkSelection && (
          <ExportConfirmationModal
            open={exportModalOpen}
            onOpenChange={setExportModalOpen}
            itemLabel="issue"
            selectedCount={selection.selectedCount}
            onConfirm={() => void handleExportIssues()}
            exporting={exporting}
          />
        )}

        <Modal
          open={bulkResolveModalOpen}
          onOpenChange={setBulkResolveModalOpen}
          dismissible
          title="Resolve issues"
          description={`Mark ${selection.selectedCount === 1 ? "this issue" : `${selection.selectedCount} issues`} as resolved. If any of these issues start occurring again we will alert you and promote them as regressed.`}
          footer={
            <>
              <Button variant="outline" onClick={() => setBulkResolveModalOpen(false)} disabled={bulkActionLoading}>
                Cancel
              </Button>
              <Button onClick={() => void handleBulkResolve()} disabled={bulkActionLoading}>
                <Icon icon={CheckIcon} size="sm" />
                Resolve {selection.selectedCount === 1 ? "Issue" : `${selection.selectedCount} Issues`}
              </Button>
            </>
          }
        >
          <div className="flex flex-col gap-3">
            <div className="flex flex-row items-center justify-between gap-4">
              <div className="flex flex-col gap-1">
                <Label htmlFor="bulk-keep-monitoring">Keep monitoring these issues</Label>
                <Text.H6 color="foregroundMuted">
                  Evaluations monitoring these issues will stay active to detect further regressions
                </Text.H6>
              </div>
              <Switch
                id="bulk-keep-monitoring"
                checked={keepMonitoring}
                onCheckedChange={setKeepMonitoring}
                disabled={bulkActionLoading}
                aria-label="Keep monitoring these issues"
              />
            </div>
          </div>
        </Modal>

        <Modal
          open={bulkIgnoreModalOpen}
          onOpenChange={setBulkIgnoreModalOpen}
          dismissible
          title="Ignore issues"
          description={`Mark ${selection.selectedCount === 1 ? "this issue" : `${selection.selectedCount} issues`} as ignored. We won't monitor or alert you about new occurrences of these issues anymore.`}
          footer={
            <>
              <CloseTrigger />
              <Button variant="destructive" onClick={() => void handleBulkIgnore()} disabled={bulkActionLoading}>
                <Icon icon={PauseIcon} size="sm" />
                Ignore {selection.selectedCount === 1 ? "Issue" : `${selection.selectedCount} Issues`}
              </Button>
            </>
          }
        />
      </Layout.Content>
      {activeIssueId ? (
        <Layout.Aside>
          <IssueDetailDrawer
            key={activeIssueId}
            projectId={project.id}
            issueId={activeIssueId}
            onClose={() => setActiveIssueId("")}
            onNextIssue={() => {
              const nextIssueId = issueIdsRef.current[currentIssueIndex + 1]
              if (nextIssueId) {
                setActiveIssueId(nextIssueId)
              }
            }}
            onPrevIssue={() => {
              const previousIssueId = issueIdsRef.current[currentIssueIndex - 1]
              if (previousIssueId) {
                setActiveIssueId(previousIssueId)
              }
            }}
            canNavigateNext={canNavigateNext}
            canNavigatePrev={canNavigatePrev}
          />
        </Layout.Aside>
      ) : null}
    </Layout>
  )
}
