import Popover from "@mui/material/Popover"
import { makeStyles, useTheme } from "@mui/styles"
import Skeleton from "@mui/material/Skeleton"
import { useMachine } from "@xstate/react"
import CodeOutlined from "@mui/icons-material/CodeOutlined"
import {
  CloseDropdown,
  OpenDropdown,
} from "components/DropdownArrows/DropdownArrows"
import { LogLine, logLineHeight } from "components/Logs/Logs"
import { PortForwardButton } from "./PortForwardButton"
import { VSCodeDesktopButton } from "components/VSCodeDesktopButton/VSCodeDesktopButton"
import {
  FC,
  useCallback,
  useEffect,
  useLayoutEffect,
  useMemo,
  useRef,
  useState,
} from "react"
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import { darcula } from "react-syntax-highlighter/dist/cjs/styles/prism"
import AutoSizer from "react-virtualized-auto-sizer"
import { FixedSizeList as List, ListOnScrollProps } from "react-window"
import { colors } from "theme/colors"
import { combineClasses } from "utils/combineClasses"
import {
  LineWithID,
  workspaceAgentLogsMachine,
} from "xServices/workspaceAgentLogs/workspaceAgentLogsXService"
import {
  Workspace,
  WorkspaceAgent,
  WorkspaceAgentMetadata,
} from "../../api/typesGenerated"
import { AppLink } from "../AppLink/AppLink"
import { SSHButton } from "../SSHButton/SSHButton"
import { Stack } from "../Stack/Stack"
import { TerminalLink } from "../TerminalLink/TerminalLink"
import { AgentLatency } from "./AgentLatency"
import { AgentMetadata } from "./AgentMetadata"
import { AgentVersion } from "./AgentVersion"
import { AgentStatus } from "./AgentStatus"
import Collapse from "@mui/material/Collapse"
import { useProxy } from "contexts/ProxyContext"

export interface AgentRowProps {
  agent: WorkspaceAgent
  workspace: Workspace
  showApps: boolean
  showBuiltinApps?: boolean
  sshPrefix?: string
  hideSSHButton?: boolean
  hideVSCodeDesktopButton?: boolean
  serverVersion: string
  onUpdateAgent: () => void
  storybookLogs?: LineWithID[]
  storybookAgentMetadata?: WorkspaceAgentMetadata[]
}

export const AgentRow: FC<AgentRowProps> = ({
  agent,
  workspace,
  showApps,
  showBuiltinApps = true,
  hideSSHButton,
  hideVSCodeDesktopButton,
  serverVersion,
  onUpdateAgent,
  storybookLogs,
  storybookAgentMetadata,
  sshPrefix,
}) => {
  const styles = useStyles()
  const [logsMachine, sendLogsEvent] = useMachine(workspaceAgentLogsMachine, {
    context: { agentID: agent.id },
    services: process.env.STORYBOOK
      ? {
          getLogs: async () => {
            return storybookLogs || []
          },
          streamLogs: () => async () => {
            // noop
          },
        }
      : undefined,
  })
  const theme = useTheme()
  const startupScriptAnchorRef = useRef<HTMLButtonElement>(null)
  const [startupScriptOpen, setStartupScriptOpen] = useState(false)
  const hasAppsToDisplay = !hideVSCodeDesktopButton || agent.apps.length > 0
  const shouldDisplayApps =
    showApps &&
    ((agent.status === "connected" && hasAppsToDisplay) ||
      agent.status === "connecting")
  const hasStartupFeatures =
    Boolean(agent.logs_length) || Boolean(logsMachine.context.logs?.length)
  const { proxy } = useProxy()

  const [showLogs, setShowLogs] = useState(
    ["starting", "start_timeout"].includes(agent.lifecycle_state) &&
      hasStartupFeatures,
  )
  useEffect(() => {
    setShowLogs(agent.lifecycle_state !== "ready" && hasStartupFeatures)
  }, [agent.lifecycle_state, hasStartupFeatures])
  // External applications can provide startup logs for an agent during it's spawn.
  // These could be Kubernetes logs, or other logs that are useful to the user.
  // For this reason, we want to fetch these logs when the agent is starting.
  useEffect(() => {
    if (agent.lifecycle_state === "starting") {
      sendLogsEvent("FETCH_LOGS")
    }
  }, [sendLogsEvent, agent.lifecycle_state])
  useEffect(() => {
    // We only want to fetch logs when they are actually shown,
    // otherwise we can make a lot of requests that aren't necessary.
    if (showLogs && logsMachine.can("FETCH_LOGS")) {
      sendLogsEvent("FETCH_LOGS")
    }
  }, [logsMachine, sendLogsEvent, showLogs])
  const logListRef = useRef<List>(null)
  const logListDivRef = useRef<HTMLDivElement>(null)
  const startupLogs = useMemo(() => {
    const allLogs = logsMachine.context.logs || []

    const logs = [...allLogs]
    if (agent.logs_overflowed) {
      logs.push({
        id: -1,
        level: "error",
        output: "Startup logs exceeded the max size of 1MB!",
        time: new Date().toISOString(),
      })
    }
    return logs
  }, [logsMachine.context.logs, agent.logs_overflowed])
  const [bottomOfLogs, setBottomOfLogs] = useState(true)
  // This is a layout effect to remove flicker when we're scrolling to the bottom.
  useLayoutEffect(() => {
    // If we're currently watching the bottom, we always want to stay at the bottom.
    if (bottomOfLogs && logListRef.current) {
      logListRef.current.scrollToItem(startupLogs.length - 1, "end")
    }
  }, [showLogs, startupLogs, logListRef, bottomOfLogs])

  // This is a bit of a hack on the react-window API to get the scroll position.
  // If we're scrolled to the bottom, we want to keep the list scrolled to the bottom.
  // This makes it feel similar to a terminal that auto-scrolls downwards!
  const handleLogScroll = useCallback(
    (props: ListOnScrollProps) => {
      if (
        props.scrollOffset === 0 ||
        props.scrollUpdateWasRequested ||
        !logListDivRef.current
      ) {
        return
      }
      // The parent holds the height of the list!
      const parent = logListDivRef.current.parentElement
      if (!parent) {
        return
      }
      const distanceFromBottom =
        logListDivRef.current.scrollHeight -
        (props.scrollOffset + parent.clientHeight)
      setBottomOfLogs(distanceFromBottom < logLineHeight)
    },
    [logListDivRef],
  )

  return (
    <Stack
      key={agent.id}
      direction="column"
      spacing={0}
      className={combineClasses([
        styles.agentRow,
        styles[`agentRow-${agent.status}`],
        styles[`agentRow-lifecycle-${agent.lifecycle_state}`],
      ])}
    >
      <div className={styles.agentInfo}>
        <div className={styles.agentNameAndStatus}>
          <div className={styles.agentNameAndInfo}>
            <AgentStatus agent={agent} />
            <div className={styles.agentName}>{agent.name}</div>
            <Stack
              direction="row"
              spacing={2}
              alignItems="baseline"
              className={styles.agentDescription}
            >
              {agent.status === "connected" && (
                <>
                  <span className={styles.agentOS}>
                    {agent.operating_system}
                  </span>
                  <AgentVersion
                    agent={agent}
                    serverVersion={serverVersion}
                    onUpdate={onUpdateAgent}
                  />
                  <AgentLatency agent={agent} />
                </>
              )}
              {agent.status === "connecting" && (
                <>
                  <Skeleton width={160} variant="text" />
                  <Skeleton width={36} variant="text" />
                </>
              )}
            </Stack>
          </div>
        </div>

        {agent.status === "connected" && (
          <div className={styles.agentButtons}>
            {shouldDisplayApps && (
              <>
                {!hideVSCodeDesktopButton && (
                  <VSCodeDesktopButton
                    userName={workspace.owner_name}
                    workspaceName={workspace.name}
                    agentName={agent.name}
                    folderPath={agent.expanded_directory}
                  />
                )}
                {agent.apps.map((app) => (
                  <AppLink
                    key={app.slug}
                    app={app}
                    agent={agent}
                    workspace={workspace}
                  />
                ))}
              </>
            )}

            {showBuiltinApps && (
              <>
                <TerminalLink
                  workspaceName={workspace.name}
                  agentName={agent.name}
                  userName={workspace.owner_name}
                />
                {!hideSSHButton && (
                  <SSHButton
                    workspaceName={workspace.name}
                    agentName={agent.name}
                    sshPrefix={sshPrefix}
                  />
                )}
                {proxy.preferredWildcardHostname &&
                  proxy.preferredWildcardHostname !== "" && (
                    <PortForwardButton
                      host={proxy.preferredWildcardHostname}
                      workspaceName={workspace.name}
                      agent={agent}
                      username={workspace.owner_name}
                    />
                  )}
              </>
            )}
          </div>
        )}

        {agent.status === "connecting" && (
          <div className={styles.agentButtons}>
            <Skeleton
              width={80}
              height={32}
              variant="rectangular"
              className={styles.buttonSkeleton}
            />
            <Skeleton
              width={110}
              height={32}
              variant="rectangular"
              className={styles.buttonSkeleton}
            />
          </div>
        )}
      </div>

      <AgentMetadata storybookMetadata={storybookAgentMetadata} agent={agent} />

      {hasStartupFeatures && (
        <div className={styles.logsPanel}>
          <Collapse in={showLogs}>
            <AutoSizer disableHeight>
              {({ width }) => (
                <List
                  ref={logListRef}
                  innerRef={logListDivRef}
                  height={256}
                  itemCount={startupLogs.length}
                  itemSize={logLineHeight}
                  width={width}
                  className={styles.startupLogs}
                  onScroll={handleLogScroll}
                >
                  {({ index, style }) => (
                    <LogLine
                      line={startupLogs[index]}
                      number={index + 1}
                      style={style}
                    />
                  )}
                </List>
              )}
            </AutoSizer>
          </Collapse>

          <div className={styles.logsPanelButtons}>
            {showLogs ? (
              <button
                className={combineClasses([
                  styles.logsPanelButton,
                  styles.toggleLogsButton,
                ])}
                onClick={() => {
                  setShowLogs((v) => !v)
                }}
              >
                <CloseDropdown />
                Hide startup logs
              </button>
            ) : (
              <button
                className={combineClasses([
                  styles.logsPanelButton,
                  styles.toggleLogsButton,
                ])}
                onClick={() => {
                  setShowLogs((v) => !v)
                }}
              >
                <OpenDropdown />
                Show startup logs
              </button>
            )}

            <button
              className={combineClasses([
                styles.logsPanelButton,
                styles.scriptButton,
              ])}
              ref={startupScriptAnchorRef}
              onClick={() => {
                setStartupScriptOpen(!startupScriptOpen)
              }}
            >
              <CodeOutlined />
              Startup script
            </button>

            <Popover
              classes={{
                paper: styles.startupScriptPopover,
              }}
              open={startupScriptOpen}
              onClose={() => setStartupScriptOpen(false)}
              anchorEl={startupScriptAnchorRef.current}
            >
              <div>
                <SyntaxHighlighter
                  style={darcula}
                  language="shell"
                  showLineNumbers
                  // Use inline styles does not work correctly
                  // https://github.com/react-syntax-highlighter/react-syntax-highlighter/issues/329
                  codeTagProps={{ style: {} }}
                  customStyle={{
                    background: theme.palette.background.default,
                    maxWidth: 600,
                    margin: 0,
                  }}
                >
                  {agent.startup_script || ""}
                </SyntaxHighlighter>
              </div>
            </Popover>
          </div>
        </div>
      )}
    </Stack>
  )
}

const useStyles = makeStyles((theme) => ({
  agentRow: {
    backgroundColor: theme.palette.background.paperLight,
    fontSize: 16,
    borderLeft: `2px solid ${theme.palette.text.secondary}`,

    "&:not(:first-of-type)": {
      borderTop: `2px solid ${theme.palette.divider}`,
    },
  },

  "agentRow-connected": {
    borderLeftColor: theme.palette.success.light,
  },

  "agentRow-disconnected": {
    borderLeftColor: theme.palette.text.secondary,
  },

  "agentRow-connecting": {
    borderLeftColor: theme.palette.info.light,
  },

  "agentRow-timeout": {
    borderLeftColor: theme.palette.warning.light,
  },

  "agentRow-lifecycle-created": {},

  "agentRow-lifecycle-starting": {
    borderLeftColor: theme.palette.info.light,
  },

  "agentRow-lifecycle-ready": {
    borderLeftColor: theme.palette.success.light,
  },

  "agentRow-lifecycle-start_timeout": {
    borderLeftColor: theme.palette.warning.light,
  },

  "agentRow-lifecycle-start_error": {
    borderLeftColor: theme.palette.error.light,
  },

  "agentRow-lifecycle-shutting_down": {
    borderLeftColor: theme.palette.info.light,
  },

  "agentRow-lifecycle-shutdown_timeout": {
    borderLeftColor: theme.palette.warning.light,
  },

  "agentRow-lifecycle-shutdown_error": {
    borderLeftColor: theme.palette.error.light,
  },

  "agentRow-lifecycle-off": {
    borderLeftColor: theme.palette.text.secondary,
  },

  agentInfo: {
    padding: theme.spacing(2, 4),
    display: "flex",
    alignItems: "center",
    gap: theme.spacing(6),
    flexWrap: "wrap",

    [theme.breakpoints.down("md")]: {
      gap: theme.spacing(2),
    },
  },

  agentNameAndInfo: {
    display: "flex",
    alignItems: "center",
    gap: theme.spacing(3),
    flexWrap: "wrap",

    [theme.breakpoints.down("md")]: {
      gap: theme.spacing(1.5),
    },
  },

  agentButtons: {
    display: "flex",
    gap: theme.spacing(1),
    justifyContent: "flex-end",
    flexWrap: "wrap",
    flex: 1,

    [theme.breakpoints.down("md")]: {
      marginLeft: 0,
      justifyContent: "flex-start",
    },
  },

  agentDescription: {
    fontSize: 14,
    color: theme.palette.text.secondary,
  },

  startupLogs: {
    maxHeight: 256,
    borderBottom: `1px solid ${theme.palette.divider}`,
    backgroundColor: theme.palette.background.paper,
    paddingTop: theme.spacing(2),

    // We need this to be able to apply the padding top from startupLogs
    "& > div": {
      position: "relative",
    },
  },

  startupScriptPopover: {
    backgroundColor: theme.palette.background.default,
  },

  agentNameAndStatus: {
    display: "flex",
    alignItems: "center",
    gap: theme.spacing(4),

    [theme.breakpoints.down("md")]: {
      width: "100%",
    },
  },

  agentName: {
    whiteSpace: "nowrap",
    overflow: "hidden",
    textOverflow: "ellipsis",
    maxWidth: 260,
    fontWeight: 600,
    fontSize: theme.spacing(2),
    flexShrink: 0,
    width: "fit-content",

    [theme.breakpoints.down("md")]: {
      overflow: "unset",
    },
  },

  agentDataGroup: {
    display: "flex",
    alignItems: "baseline",
    gap: theme.spacing(6),
  },

  agentData: {
    display: "flex",
    flexDirection: "column",
    fontSize: 12,

    "& > *:first-of-type": {
      fontWeight: 500,
      color: theme.palette.text.secondary,
    },
  },

  logsPanel: {
    borderTop: `1px solid ${theme.palette.divider}`,
  },

  logsPanelButtons: {
    display: "flex",
  },

  logsPanelButton: {
    textAlign: "left",
    background: "transparent",
    border: 0,
    fontFamily: "inherit",
    padding: theme.spacing(1.5, 4),
    color: theme.palette.text.secondary,
    cursor: "pointer",
    display: "flex",
    alignItems: "center",
    gap: theme.spacing(1),
    whiteSpace: "nowrap",

    "&:hover": {
      color: theme.palette.text.primary,
      backgroundColor: colors.gray[14],
    },

    "& svg": {
      color: "inherit",
    },
  },

  toggleLogsButton: {
    width: "100%",
  },

  buttonSkeleton: {
    borderRadius: 4,
  },

  agentErrorMessage: {
    fontSize: 12,
    fontWeight: 400,
    marginTop: theme.spacing(0.5),
    color: theme.palette.warning.light,
  },

  scriptButton: {
    "& svg": {
      width: theme.spacing(2),
      height: theme.spacing(2),
    },
  },

  agentOS: {
    textTransform: "capitalize",
  },
}))
