import { useEffect, useState } from 'react'
import {
  type Action,
  type ActionGroupSchedule,
  type ActionLogFile,
  type ActionLogFileInfo,
  type ActionGroupRunRecord,
  type MeResponse,
  type Node,
  type NodeActionGroup,
  type NodeActionItem,
  cloneActionGroupItem,
  createActionGroup,
  createActionGroupItem,
  deleteActionGroup,
  deleteActionGroupItem,
  fetchActionGroups,
  fetchActionLogFile,
  fetchActionLogs,
  fetchActions,
  fetchMe,
  fetchNode,
  patchActionGroup,
  patchActionGroupItem,
  reorderActionGroupItems,
  runActionGroup,
  runActionGroupItem,
} from './api/client'
import { SECTION_TITLES } from './documentTitle'
import {
  type NodeDetailTabId,
  type ResourceSectionId,
  navigateTo,
  pathFor,
} from './navigation'

const WEEKDAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] as const

const NODE_DETAIL_TABS: ReadonlyArray<{ id: NodeDetailTabId; label: string }> = [
  { id: 'overview', label: 'Overview' },
  { id: 'actions', label: 'Actions' },
  { id: 'logs', label: 'Logs' },
]

function defaultSchedule(): ActionGroupSchedule {
  return {
    enabled: false,
    kind: 'interval',
    every_seconds: 3600,
    times: [],
  }
}

function itemDisplayName(
  item: NodeActionItem,
  libraryById: Map<string, Action>,
): string {
  if (item.source === 'local') {
    return item.name || 'Untitled action'
  }
  const libraryAction = libraryById.get(item.library_action_id || '')
  return libraryAction?.name || `Library ${item.library_action_id || 'action'}`
}

export function NodeDetailPanel({
  section,
  nodeId,
  activeTab,
}: {
  section: ResourceSectionId
  nodeId: string
  activeTab: NodeDetailTabId
}) {
  const [me, setMe] = useState<MeResponse | null>(null)
  const [node, setNode] = useState<Node | null>(null)
  const [loadError, setLoadError] = useState<string | null>(null)
  const [isLoading, setIsLoading] = useState(true)

  useEffect(() => {
    let isCancelled = false

    async function load() {
      setIsLoading(true)
      setLoadError(null)
      try {
        const [meResponse, nodeResponse] = await Promise.all([
          fetchMe(),
          fetchNode(nodeId),
        ])
        if (!isCancelled) {
          setMe(meResponse)
          setNode(nodeResponse.node)
        }
      } catch (err) {
        if (!isCancelled) {
          setLoadError(err instanceof Error ? err.message : 'Failed to load node')
          setNode(null)
        }
      } finally {
        if (!isCancelled) {
          setIsLoading(false)
        }
      }
    }

    void load()
    return () => {
      isCancelled = true
    }
  }, [nodeId])

  function goToTab(tab: NodeDetailTabId) {
    navigateTo(pathFor(section, 'overview', 'overview', nodeId, tab))
  }

  function goBack() {
    navigateTo(pathFor(section))
  }

  if (isLoading) {
    return <p className="config-hint">Loading…</p>
  }

  if (loadError || !node) {
    return (
      <div className="config-panel">
        <button type="button" className="node-detail-back" onClick={goBack}>
          ← Back to {SECTION_TITLES[section]}
        </button>
        <p className="config-error">{loadError || 'Node not found'}</p>
      </div>
    )
  }

  const canCreate = Boolean(me?.permissions?.includes('actions.create'))
  const canUpdate = Boolean(me?.permissions?.includes('actions.update'))
  const canDelete = Boolean(me?.permissions?.includes('actions.delete'))
  const canRun = Boolean(me?.permissions?.includes('jobs.run'))
  const canReadLogs = Boolean(me?.permissions?.includes('logs.read'))

  return (
    <div className="config-panel node-detail-panel">
      <button type="button" className="node-detail-back" onClick={goBack}>
        ← Back to {SECTION_TITLES[section]}
      </button>
      <header className="node-detail-header">
        <h2>{node.name}</h2>
        <p className="config-hint">
          {node.host_ip} · {node.username} · {node.group_name}
        </p>
        <code className="node-id">{node.id}</code>
      </header>

      <div
        className="config-tabs"
        role="tablist"
        aria-label={`${node.name} detail`}
      >
        {NODE_DETAIL_TABS.map((tab) => {
          const isSelected = activeTab === tab.id
          return (
            <button
              key={tab.id}
              type="button"
              role="tab"
              className={
                isSelected ? 'config-tab config-tab-active' : 'config-tab'
              }
              aria-selected={isSelected}
              onClick={() => goToTab(tab.id)}
            >
              {tab.label}
            </button>
          )
        })}
      </div>

      <div className="config-tab-panel" role="tabpanel">
        {activeTab === 'overview' ? (
          <NodeDetailOverviewTab node={node} />
        ) : null}
        {activeTab === 'actions' ? (
          <NodeActionsTab
            nodeId={node.id}
            canCreate={canCreate}
            canUpdate={canUpdate}
            canDelete={canDelete}
            canRun={canRun}
          />
        ) : null}
        {activeTab === 'logs' ? (
          <NodeActionLogsTab nodeId={node.id} canRead={canReadLogs} />
        ) : null}
      </div>
    </div>
  )
}

function NodeDetailOverviewTab({ node }: { node: Node }) {
  return (
    <dl className="node-detail-overview">
      <div>
        <dt>Name</dt>
        <dd>{node.name}</dd>
      </div>
      <div>
        <dt>Host IP</dt>
        <dd>{node.host_ip}</dd>
      </div>
      <div>
        <dt>Username</dt>
        <dd>{node.username}</dd>
      </div>
      <div>
        <dt>Group</dt>
        <dd>{node.group_name}</dd>
      </div>
      <div>
        <dt>Key algorithm</dt>
        <dd>{node.key_algo}</dd>
      </div>
      <div>
        <dt>Created</dt>
        <dd>{new Date(node.created_at).toLocaleString()}</dd>
      </div>
    </dl>
  )
}

function NodeActionsTab({
  nodeId,
  canCreate,
  canUpdate,
  canDelete,
  canRun,
}: {
  nodeId: string
  canCreate: boolean
  canUpdate: boolean
  canDelete: boolean
  canRun: boolean
}) {
  const [groups, setGroups] = useState<ReadonlyArray<NodeActionGroup>>([])
  const [library, setLibrary] = useState<ReadonlyArray<Action>>([])
  const [isLoading, setIsLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)
  const [newGroupName, setNewGroupName] = useState('')
  const [busyKey, setBusyKey] = useState<string | null>(null)
  const [lastRun, setLastRun] = useState<ActionGroupRunRecord | null>(null)

  const libraryById = new Map(library.map((action) => [action.id, action]))

  async function reload() {
    const [groupsResponse, actionsResponse] = await Promise.all([
      fetchActionGroups(nodeId),
      fetchActions(),
    ])
    setGroups(groupsResponse.groups)
    setLibrary(actionsResponse.actions)
  }

  useEffect(() => {
    let isCancelled = false

    async function load() {
      setIsLoading(true)
      setError(null)
      try {
        await reload()
      } catch (err) {
        if (!isCancelled) {
          setError(err instanceof Error ? err.message : 'Failed to load actions')
        }
      } finally {
        if (!isCancelled) {
          setIsLoading(false)
        }
      }
    }

    void load()
    return () => {
      isCancelled = true
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [nodeId])

  async function handleCreateGroup() {
    const name = newGroupName.trim()
    if (!name) {
      return
    }
    setBusyKey('create-group')
    setError(null)
    try {
      await createActionGroup(nodeId, {
        name,
        schedule: defaultSchedule(),
      })
      setNewGroupName('')
      await reload()
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to create group')
    } finally {
      setBusyKey(null)
    }
  }

  function replaceGroup(updated: NodeActionGroup) {
    setGroups((previous) =>
      previous.map((group) => (group.id === updated.id ? updated : group)),
    )
  }

  if (isLoading) {
    return <p className="config-hint">Loading action groups…</p>
  }

  return (
    <div className="node-actions-tab">
      {error ? <p className="config-error">{error}</p> : null}
      {canCreate ? (
        <div className="node-action-group-create">
          <input
            type="text"
            placeholder="New action group name"
            value={newGroupName}
            onChange={(event) => setNewGroupName(event.target.value)}
            aria-label="New action group name"
          />
          <button
            type="button"
            disabled={busyKey === 'create-group' || !newGroupName.trim()}
            onClick={() => {
              void handleCreateGroup()
            }}
          >
            Add Action Group
          </button>
        </div>
      ) : (
        <p className="config-hint">
          You need <code>actions.create</code> to add action groups.
        </p>
      )}

      {groups.length === 0 ? (
        <p className="config-hint">No action groups on this host yet.</p>
      ) : (
        <ul className="node-action-group-list">
          {groups.map((group) => (
            <li key={group.id} className="node-action-group-card">
              <ActionGroupEditor
                nodeId={nodeId}
                group={group}
                library={library}
                libraryById={libraryById}
                canCreate={canCreate}
                canUpdate={canUpdate}
                canDelete={canDelete}
                canRun={canRun}
                busyKey={busyKey}
                setBusyKey={setBusyKey}
                setError={setError}
                onGroupChange={replaceGroup}
                onGroupsChange={setGroups}
                onRunComplete={setLastRun}
              />
            </li>
          ))}
        </ul>
      )}

      {lastRun ? (
        <div className="node-action-run-result">
          <h3>Last run result</h3>
          <p className="config-hint">
            {lastRun.group_name} · {lastRun.trigger} ·{' '}
            {new Date(lastRun.finished_at).toLocaleString()}
          </p>
          {lastRun.actions.map((actionResult) => (
            <details key={actionResult.item_id} open>
              <summary>
                {actionResult.action_name} (exit {actionResult.exit_code})
                {actionResult.error ? ` — ${actionResult.error}` : ''}
              </summary>
              {actionResult.stdout ? (
                <pre className="action-log-pre">{actionResult.stdout}</pre>
              ) : null}
              {actionResult.stderr ? (
                <pre className="action-log-pre action-log-stderr">
                  {actionResult.stderr}
                </pre>
              ) : null}
            </details>
          ))}
        </div>
      ) : null}
    </div>
  )
}

function ActionGroupEditor({
  nodeId,
  group,
  library,
  libraryById,
  canCreate,
  canUpdate,
  canDelete,
  canRun,
  busyKey,
  setBusyKey,
  setError,
  onGroupChange,
  onGroupsChange,
  onRunComplete,
}: {
  nodeId: string
  group: NodeActionGroup
  library: ReadonlyArray<Action>
  libraryById: Map<string, Action>
  canCreate: boolean
  canUpdate: boolean
  canDelete: boolean
  canRun: boolean
  busyKey: string | null
  setBusyKey: (key: string | null) => void
  setError: (message: string | null) => void
  onGroupChange: (group: NodeActionGroup) => void
  onGroupsChange: (groups: ReadonlyArray<NodeActionGroup>) => void
  onRunComplete: (run: ActionGroupRunRecord) => void
}) {
  const [nameDraft, setNameDraft] = useState(group.name)
  const [scheduleDraft, setScheduleDraft] = useState<ActionGroupSchedule>(
    group.schedule || defaultSchedule(),
  )
  const [libraryPick, setLibraryPick] = useState('')
  const [customName, setCustomName] = useState('')
  const [customBody, setCustomBody] = useState('')
  const [customKind, setCustomKind] = useState<'shell' | 'script'>('shell')
  const [editingItemId, setEditingItemId] = useState<string | null>(null)
  const [editName, setEditName] = useState('')
  const [editBody, setEditBody] = useState('')

  useEffect(() => {
    setNameDraft(group.name)
    setScheduleDraft(group.schedule || defaultSchedule())
  }, [group])

  async function saveSchedule() {
    setBusyKey(`save-${group.id}`)
    setError(null)
    try {
      const response = await patchActionGroup(nodeId, group.id, {
        name: nameDraft.trim(),
        schedule: scheduleDraft,
      })
      onGroupChange(response.group)
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to save group')
    } finally {
      setBusyKey(null)
    }
  }

  async function handleDeleteGroup() {
    if (!window.confirm(`Delete action group “${group.name}”?`)) {
      return
    }
    setBusyKey(`delete-group-${group.id}`)
    setError(null)
    try {
      const response = await deleteActionGroup(nodeId, group.id)
      onGroupsChange(response.groups)
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to delete group')
    } finally {
      setBusyKey(null)
    }
  }

  async function handleAddLibrary() {
    if (!libraryPick) {
      return
    }
    setBusyKey(`add-lib-${group.id}`)
    setError(null)
    try {
      const response = await createActionGroupItem(nodeId, group.id, {
        source: 'library',
        library_action_id: libraryPick,
      })
      onGroupChange(response.group)
      setLibraryPick('')
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to add action')
    } finally {
      setBusyKey(null)
    }
  }

  async function handleAddCustom() {
    setBusyKey(`add-custom-${group.id}`)
    setError(null)
    try {
      const response = await createActionGroupItem(nodeId, group.id, {
        source: 'local',
        name: customName.trim(),
        description: '',
        kind: customKind,
        body: customBody.trim(),
        env: [],
      })
      onGroupChange(response.group)
      setCustomName('')
      setCustomBody('')
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to add custom action')
    } finally {
      setBusyKey(null)
    }
  }

  async function moveItem(itemId: string, direction: -1 | 1) {
    const ids = group.items.map((item) => item.id)
    const index = ids.indexOf(itemId)
    const nextIndex = index + direction
    if (index < 0 || nextIndex < 0 || nextIndex >= ids.length) {
      return
    }
    const reordered = [...ids]
    const [moved] = reordered.splice(index, 1)
    reordered.splice(nextIndex, 0, moved)
    setBusyKey(`order-${group.id}`)
    setError(null)
    try {
      const response = await reorderActionGroupItems(nodeId, group.id, reordered)
      onGroupChange(response.group)
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to reorder')
    } finally {
      setBusyKey(null)
    }
  }

  async function handleRunGroup() {
    setBusyKey(`run-group-${group.id}`)
    setError(null)
    try {
      const response = await runActionGroup(nodeId, group.id)
      onRunComplete(response.run)
      const refreshed = await fetchActionGroups(nodeId)
      onGroupsChange(refreshed.groups)
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to run group')
    } finally {
      setBusyKey(null)
    }
  }

  return (
    <div>
      <div className="node-action-group-heading">
        {canUpdate ? (
          <input
            type="text"
            value={nameDraft}
            onChange={(event) => setNameDraft(event.target.value)}
            aria-label="Action group name"
          />
        ) : (
          <strong>{group.name}</strong>
        )}
        <div className="node-action-group-toolbar">
          {canRun ? (
            <button
              type="button"
              disabled={busyKey === `run-group-${group.id}` || group.items.length === 0}
              onClick={() => {
                void handleRunGroup()
              }}
            >
              Run group
            </button>
          ) : null}
          {canUpdate ? (
            <button
              type="button"
              disabled={busyKey === `save-${group.id}`}
              onClick={() => {
                void saveSchedule()
              }}
            >
              Save
            </button>
          ) : null}
          {canDelete ? (
            <button
              type="button"
              className="danger"
              disabled={busyKey === `delete-group-${group.id}`}
              onClick={() => {
                void handleDeleteGroup()
              }}
            >
              Delete group
            </button>
          ) : null}
        </div>
      </div>

      {group.last_run_at ? (
        <p className="config-hint">
          Last run: {new Date(group.last_run_at).toLocaleString()}
        </p>
      ) : (
        <p className="config-hint">Never run</p>
      )}

      <fieldset className="node-action-schedule" disabled={!canUpdate}>
        <legend>Schedule</legend>
        <label className="checkbox-row">
          <input
            type="checkbox"
            checked={scheduleDraft.enabled}
            onChange={(event) =>
              setScheduleDraft((previous) => ({
                ...previous,
                enabled: event.target.checked,
              }))
            }
          />
          Enabled
        </label>
        <label>
          Kind
          <select
            value={scheduleDraft.kind}
            onChange={(event) =>
              setScheduleDraft((previous) => ({
                ...previous,
                kind: event.target.value as ActionGroupSchedule['kind'],
              }))
            }
          >
            <option value="interval">Interval</option>
            <option value="times">Times of day / week</option>
          </select>
        </label>
        {scheduleDraft.kind === 'interval' ? (
          <label>
            Every (seconds)
            <input
              type="number"
              min={1}
              value={scheduleDraft.every_seconds || 3600}
              onChange={(event) =>
                setScheduleDraft((previous) => ({
                  ...previous,
                  every_seconds: Number(event.target.value) || 1,
                }))
              }
            />
          </label>
        ) : (
          <ScheduleTimesEditor
            times={scheduleDraft.times || []}
            onChange={(times) =>
              setScheduleDraft((previous) => ({ ...previous, times }))
            }
          />
        )}
      </fieldset>

      <h4>Actions</h4>
      {group.items.length === 0 ? (
        <p className="config-hint">No actions in this group yet.</p>
      ) : (
        <ol className="node-action-item-list">
          {group.items.map((item, index) => (
            <li key={item.id} className="node-action-item">
              <div className="node-action-item-heading">
                <span>
                  {itemDisplayName(item, libraryById)}{' '}
                  <span className="config-hint">({item.source})</span>
                </span>
                <div className="node-action-item-toolbar">
                  {canUpdate ? (
                    <>
                      <button
                        type="button"
                        disabled={index === 0 || Boolean(busyKey)}
                        onClick={() => {
                          void moveItem(item.id, -1)
                        }}
                        aria-label="Move up"
                      >
                        ↑
                      </button>
                      <button
                        type="button"
                        disabled={
                          index === group.items.length - 1 || Boolean(busyKey)
                        }
                        onClick={() => {
                          void moveItem(item.id, 1)
                        }}
                        aria-label="Move down"
                      >
                        ↓
                      </button>
                    </>
                  ) : null}
                  {canRun ? (
                    <button
                      type="button"
                      disabled={busyKey === `run-item-${item.id}`}
                      onClick={() => {
                        void (async () => {
                          setBusyKey(`run-item-${item.id}`)
                          setError(null)
                          try {
                            const response = await runActionGroupItem(
                              nodeId,
                              group.id,
                              item.id,
                            )
                            onRunComplete(response.run)
                          } catch (err) {
                            setError(
                              err instanceof Error
                                ? err.message
                                : 'Failed to run action',
                            )
                          } finally {
                            setBusyKey(null)
                          }
                        })()
                      }}
                    >
                      Run
                    </button>
                  ) : null}
                  {item.source === 'library' && canCreate ? (
                    <button
                      type="button"
                      disabled={busyKey === `clone-${item.id}`}
                      onClick={() => {
                        void (async () => {
                          setBusyKey(`clone-${item.id}`)
                          setError(null)
                          try {
                            const response = await cloneActionGroupItem(
                              nodeId,
                              group.id,
                              item.id,
                            )
                            onGroupChange(response.group)
                          } catch (err) {
                            setError(
                              err instanceof Error
                                ? err.message
                                : 'Failed to clone',
                            )
                          } finally {
                            setBusyKey(null)
                          }
                        })()
                      }}
                    >
                      Clone to customize
                    </button>
                  ) : null}
                  {item.source === 'local' && canUpdate ? (
                    <button
                      type="button"
                      onClick={() => {
                        setEditingItemId(item.id)
                        setEditName(item.name || '')
                        setEditBody(item.body || '')
                      }}
                    >
                      Edit
                    </button>
                  ) : null}
                  {canDelete ? (
                    <button
                      type="button"
                      className="danger"
                      disabled={busyKey === `delete-item-${item.id}`}
                      onClick={() => {
                        void (async () => {
                          setBusyKey(`delete-item-${item.id}`)
                          setError(null)
                          try {
                            const response = await deleteActionGroupItem(
                              nodeId,
                              group.id,
                              item.id,
                            )
                            onGroupChange(response.group)
                          } catch (err) {
                            setError(
                              err instanceof Error
                                ? err.message
                                : 'Failed to delete action',
                            )
                          } finally {
                            setBusyKey(null)
                          }
                        })()
                      }}
                    >
                      Remove
                    </button>
                  ) : null}
                </div>
              </div>
              {editingItemId === item.id ? (
                <div className="node-action-item-edit">
                  <label>
                    Name
                    <input
                      type="text"
                      value={editName}
                      onChange={(event) => setEditName(event.target.value)}
                    />
                  </label>
                  <label>
                    Body
                    <textarea
                      value={editBody}
                      rows={4}
                      onChange={(event) => setEditBody(event.target.value)}
                    />
                  </label>
                  <div className="node-action-item-toolbar">
                    <button
                      type="button"
                      onClick={() => {
                        void (async () => {
                          setBusyKey(`edit-${item.id}`)
                          setError(null)
                          try {
                            const response = await patchActionGroupItem(
                              nodeId,
                              group.id,
                              item.id,
                              { name: editName, body: editBody },
                            )
                            onGroupChange(response.group)
                            setEditingItemId(null)
                          } catch (err) {
                            setError(
                              err instanceof Error
                                ? err.message
                                : 'Failed to save action',
                            )
                          } finally {
                            setBusyKey(null)
                          }
                        })()
                      }}
                    >
                      Save action
                    </button>
                    <button
                      type="button"
                      onClick={() => setEditingItemId(null)}
                    >
                      Cancel
                    </button>
                  </div>
                </div>
              ) : null}
            </li>
          ))}
        </ol>
      )}

      {canCreate ? (
        <div className="node-action-adders">
          <div className="node-action-add-row">
            <select
              value={libraryPick}
              onChange={(event) => setLibraryPick(event.target.value)}
              aria-label="Library action"
            >
              <option value="">Add from library…</option>
              {library.map((action) => (
                <option key={action.id} value={action.id}>
                  {action.name}
                </option>
              ))}
            </select>
            <button
              type="button"
              disabled={!libraryPick || busyKey === `add-lib-${group.id}`}
              onClick={() => {
                void handleAddLibrary()
              }}
            >
              Add library action
            </button>
          </div>
          <div className="node-action-add-custom">
            <input
              type="text"
              placeholder="Custom action name"
              value={customName}
              onChange={(event) => setCustomName(event.target.value)}
            />
            <select
              value={customKind}
              onChange={(event) =>
                setCustomKind(event.target.value as 'shell' | 'script')
              }
            >
              <option value="shell">Shell</option>
              <option value="script">Script</option>
            </select>
            <textarea
              placeholder="Command or script body"
              value={customBody}
              rows={3}
              onChange={(event) => setCustomBody(event.target.value)}
            />
            <button
              type="button"
              disabled={
                !customName.trim() ||
                !customBody.trim() ||
                busyKey === `add-custom-${group.id}`
              }
              onClick={() => {
                void handleAddCustom()
              }}
            >
              Add custom action
            </button>
          </div>
        </div>
      ) : null}
    </div>
  )
}

function ScheduleTimesEditor({
  times,
  onChange,
}: {
  times: ReadonlyArray<{ time: string; days_of_week?: ReadonlyArray<number> }>
  onChange: (
    times: Array<{ time: string; days_of_week?: ReadonlyArray<number> }>,
  ) => void
}) {
  function updateAt(
    index: number,
    next: { time: string; days_of_week?: ReadonlyArray<number> },
  ) {
    const copy = times.map((entry) => ({
      time: entry.time,
      days_of_week: entry.days_of_week ? [...entry.days_of_week] : undefined,
    }))
    copy[index] = {
      time: next.time,
      days_of_week: next.days_of_week ? [...next.days_of_week] : undefined,
    }
    onChange(copy)
  }

  return (
    <div className="schedule-times-editor">
      {times.map((spec, index) => (
        <div key={index} className="schedule-time-row">
          <input
            type="time"
            value={spec.time || '00:00'}
            onChange={(event) =>
              updateAt(index, { ...spec, time: event.target.value })
            }
          />
          <div className="schedule-weekdays">
            {WEEKDAY_LABELS.map((label, day) => {
              const selected = (spec.days_of_week || []).includes(day)
              return (
                <label key={label} className="checkbox-row">
                  <input
                    type="checkbox"
                    checked={selected}
                    onChange={(event) => {
                      const current = new Set(spec.days_of_week || [])
                      if (event.target.checked) {
                        current.add(day)
                      } else {
                        current.delete(day)
                      }
                      updateAt(index, {
                        time: spec.time,
                        days_of_week: Array.from(current).sort(),
                      })
                    }}
                  />
                  {label}
                </label>
              )
            })}
          </div>
          <button
            type="button"
            onClick={() => onChange(times.filter((_, i) => i !== index))}
          >
            Remove time
          </button>
        </div>
      ))}
      <button
        type="button"
        onClick={() => onChange([...times, { time: '09:00', days_of_week: [] }])}
      >
        Add time
      </button>
      <p className="config-hint">
        Leave weekdays unchecked to run every day at that time.
      </p>
    </div>
  )
}

function NodeActionLogsTab({
  nodeId,
  canRead,
}: {
  nodeId: string
  canRead: boolean
}) {
  const [files, setFiles] = useState<ReadonlyArray<ActionLogFileInfo>>([])
  const [selectedFilename, setSelectedFilename] = useState<string | null>(null)
  const [logFile, setLogFile] = useState<ActionLogFile | null>(null)
  const [isLoading, setIsLoading] = useState(true)
  const [isLoadingContent, setIsLoadingContent] = useState(false)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    if (!canRead) {
      setIsLoading(false)
      return
    }
    let isCancelled = false

    async function load() {
      setIsLoading(true)
      setError(null)
      try {
        const response = await fetchActionLogs(nodeId)
        if (!isCancelled) {
          setFiles(response.files)
          if (response.files.length > 0) {
            setSelectedFilename(response.files[0].filename)
          }
        }
      } catch (err) {
        if (!isCancelled) {
          setError(err instanceof Error ? err.message : 'Failed to list logs')
        }
      } finally {
        if (!isCancelled) {
          setIsLoading(false)
        }
      }
    }

    void load()
    return () => {
      isCancelled = true
    }
  }, [nodeId, canRead])

  useEffect(() => {
    if (!selectedFilename || !canRead) {
      setLogFile(null)
      return
    }
    let isCancelled = false

    async function loadContent() {
      setIsLoadingContent(true)
      setError(null)
      try {
        const response = await fetchActionLogFile(nodeId, selectedFilename!)
        if (!isCancelled) {
          setLogFile(response.log)
        }
      } catch (err) {
        if (!isCancelled) {
          setError(err instanceof Error ? err.message : 'Failed to load log')
          setLogFile(null)
        }
      } finally {
        if (!isCancelled) {
          setIsLoadingContent(false)
        }
      }
    }

    void loadContent()
    return () => {
      isCancelled = true
    }
  }, [nodeId, selectedFilename, canRead])

  if (!canRead) {
    return (
      <p className="config-hint">
        You need the <code>logs.read</code> permission to view action logs.
      </p>
    )
  }

  if (isLoading) {
    return <p className="config-hint">Loading logs…</p>
  }

  return (
    <div className="node-action-logs-layout">
      {error ? <p className="config-error">{error}</p> : null}
      <aside className="node-action-logs-list" aria-label="Log files">
        {files.length === 0 ? (
          <p className="config-hint">No log files yet.</p>
        ) : (
          <ul>
            {files.map((file) => (
              <li key={file.filename}>
                <button
                  type="button"
                  className={
                    selectedFilename === file.filename
                      ? 'log-file-button log-file-button-active'
                      : 'log-file-button'
                  }
                  onClick={() => setSelectedFilename(file.filename)}
                >
                  <strong>{file.group_name}</strong>
                  <span className="config-hint">{file.filename}</span>
                </button>
              </li>
            ))}
          </ul>
        )}
      </aside>
      <section className="node-action-logs-content" aria-label="Log content">
        {!selectedFilename ? (
          <p className="config-hint">Select a log file.</p>
        ) : isLoadingContent ? (
          <p className="config-hint">Loading…</p>
        ) : logFile ? (
          <div>
            <h3>{logFile.group_name}</h3>
            {logFile.runs.length === 0 ? (
              <p className="config-hint">No runs recorded.</p>
            ) : (
              [...logFile.runs].reverse().map((run) => (
                <details key={run.id} className="action-log-run">
                  <summary>
                    {new Date(run.started_at).toLocaleString()} · {run.trigger}
                    {run.actor ? ` · ${run.actor}` : ''}
                  </summary>
                  <pre className="action-log-pre">
                    {JSON.stringify(run, null, 2)}
                  </pre>
                </details>
              ))
            )}
          </div>
        ) : (
          <p className="config-hint">No content.</p>
        )}
      </section>
    </div>
  )
}
