import { useState, type ReactNode } from 'react';
import { Folder, FolderInput, Hash, Pencil, Play, Plus, RefreshCw, Stethoscope, Trash2, TriangleAlert } from 'lucide-react';
import {
  Badge,
  Button,
  Card,
  ChannelLogo,
  Checkbox,
  IconButton,
  ProgressBar,
  Spinner,
  StatusDot
} from '../components';
import {
  bulkDeleteChannels,
  bulkMoveChannelsToGroup,
  bulkRenumberChannels,
  createChannel,
  deleteChannel,
  findLogoBugWatermark,
  getFfmpegSettings,
  getWatermarks,
  messageFromError,
  useChannelsScreenQuery,
  type ChannelPreviewAvailability,
  type ChannelState,
  type ChannelSummary,
  type CreateChannelRequest
} from '../api';
import { navigateToPath } from '../routing';
import { usePrimaryAction } from '../primaryAction';
import { ChannelPreviewPanel } from './channels/ChannelPreviewPanel';

type ChannelViewFilter = 'all' | 'onair' | 'disabled' | 'problems';

// #415: per-channel fault taxonomy derived server-side from the built timeline (health.status).
// The SPA reads the verdict; it no longer derives eligibility from raw playoutCount. Each row's badge
// still spells out the specific fault (below); the filter rolls them up under "Problems".
// Design: docs/superpowers/specs/2026-07-23-channel-fault-detection-design.md; decision: api.channel-health-object.
function hasProblems(channel: ChannelSummary): boolean {
  return channel.health?.status === 'Problems';
}

const FAULT_LABELS: Record<string, string> = {
  NoPlayout: 'No playout',
  NeverBuilt: 'Never built',
  BuildFailed: 'Build failed',
  EmptyUpcoming: 'Empty schedule',
  BrokenSource: 'Broken source'
};

const streamingModeBadgeTones: Record<string, 'accent' | 'neutral'> = {
  'HLS Direct': 'neutral',
  'HLS Segmenter': 'accent',
  'MPEG-TS': 'accent',
  'MPEG-TS (Legacy)': 'neutral'
};

function stateByChannelId(states: ChannelState[]): Map<number, ChannelState> {
  return new Map(states.map((state) => [state.channelId, state]));
}

function sortedChannels(channels: ChannelSummary[]): ChannelSummary[] {
  return [...channels].sort((left, right) => left.sortNumber - right.sortNumber || left.number.localeCompare(right.number));
}

function groupedChannels(channels: ChannelSummary[]): Array<{ group: string; rows: ChannelSummary[] }> {
  const groups: Array<{ group: string; rows: ChannelSummary[] }> = [];
  const indexes = new Map<string, number>();

  for (const channel of channels) {
    const group = channel.group || 'Ungrouped';
    const index = indexes.get(group);

    if (index == null) {
      indexes.set(group, groups.length);
      groups.push({ group, rows: [channel] });
    } else {
      groups[index].rows.push(channel);
    }
  }

  return groups;
}

// Inlined from App.tsx's Dashboard-owned progressFromNowPlaying during the #244 extraction:
// the Channels domain reuses the identical start/finish/now percentage math, and ChannelState's
// nowPlaying carries the same startUtc/finishUtc shape, so inlining keeps the screen self-contained
// with no cross-import back into App.tsx (behavior-identical).
function progressFromChannelState(state: ChannelState): number | null {
  const nowPlaying = state.nowPlaying;

  if (!nowPlaying) {
    return null;
  }

  const start = new Date(nowPlaying.startUtc).getTime();
  const finish = new Date(nowPlaying.finishUtc).getTime();
  const now = Date.now();

  if (!Number.isFinite(start) || !Number.isFinite(finish) || finish <= start) {
    return null;
  }

  return Math.round(((now - start) / (finish - start)) * 100);
}

function Marker({ children, title }: { children: ReactNode; title: string }) {
  return (
    <span className="ctv-channel-marker" title={title}>
      {children}
    </span>
  );
}

function ChannelsLoadingState() {
  return (
    <Card>
      <div className="ctv-dashboard-state">
        <Spinner size={20} tone="accent" />
        <span>Loading channels</span>
      </div>
    </Card>
  );
}

function ChannelsErrorState({ error, refresh }: { error: string; refresh: () => void }) {
  return (
    <Card
      title={<h2>Channels unavailable</h2>}
      subtitle="Live API request failed"
      actions={<Button onClick={refresh} startIcon={<RefreshCw aria-hidden="true" size={14} />} variant="secondary">Retry</Button>}
    >
      <div className="ctv-dashboard-error">
        <span>API request failed</span>
        <strong>{error}</strong>
      </div>
    </Card>
  );
}

// Empty lineup (fresh install) must still let the user create their first channel — both the
// library-to-lineup ChannelBuilder flow ("Add Channel") and the bare-create flow ("New blank
// channel"). The bare-create handler is passed in from ChannelsScreen so the defaults (number =
// max+1, which degenerates to 1 on an empty lineup; group "ErsatzTV"; default ffmpeg profile) are
// identical to the action bar's. Regression for #212 (reopened): the old early return rendered a
// dead-end card before the action bar, so a fresh install could never create a channel.
// mutationError is rendered here too: a fresh install may have no default ffmpeg profile, so
// bare-create can 4xx — the non-empty screen shows that alert, and this branch must not swallow it
// (a silent spinner on the one screen that exists to bootstrap the first channel is the worst place
// to hide the failure).
function ChannelsEmptyState({
  creatingBlank,
  mutationError,
  onCreateBlank
}: {
  creatingBlank: boolean;
  mutationError: string | null;
  onCreateBlank: () => void;
}) {
  return (
    <Card title={<h2>No channels</h2>} subtitle="No channels yet — create your first one.">
      <div className="ctv-channels-empty">
        <p className="ctv-dashboard-empty">No channels yet</p>
        {mutationError && (
          <div className="ctv-channels-error" role="alert">
            <TriangleAlert aria-hidden="true" size={15} />
            <span>{mutationError}</span>
          </div>
        )}
        <div className="ctv-channels-empty-actions">
          <Button
            disabled={creatingBlank}
            loading={creatingBlank}
            onClick={onCreateBlank}
            startIcon={<Plus aria-hidden="true" size={14} />}
            variant="secondary"
          >
            New blank channel
          </Button>
          <Button
            startIcon={<Plus aria-hidden="true" size={14} />}
            onClick={() => navigateToPath('/app/new-channel')}
          >
            Add Channel
          </Button>
        </div>
      </div>
    </Card>
  );
}

export function ChannelsScreen() {
  const query = useChannelsScreenQuery();
  const [filter, setFilter] = useState<ChannelViewFilter>('all');
  const [selectedIds, setSelectedIds] = useState<Set<number>>(() => new Set());
  const [mutationError, setMutationError] = useState<string | null>(null);
  const [mutating, setMutating] = useState(false);
  const [creatingBlank, setCreatingBlank] = useState(false);
  const [previewChannelId, setPreviewChannelId] = useState<null | number>(null);

  // TopBar "Add Channel" primary action → the library-to-lineup builder.
  usePrimaryAction('channels', () => navigateToPath('/app/new-channel'));

  if (query.status === 'loading') {
    return <ChannelsLoadingState />;
  }

  if (query.status === 'error') {
    return <ChannelsErrorState error={query.error} refresh={query.refresh} />;
  }

  const channels = sortedChannels(query.data.channels);
  const statesById = stateByChannelId(query.data.channelStates);
  const onAirCount = query.data.channelStates.filter((state) => state.onAir).length;
  const disabledCount = channels.filter((channel) => !channel.isEnabled).length;
  const problemsCount = channels.filter(hasProblems).length;
  const visibleChannels = channels.filter((channel) => {
    if (filter === 'onair') {
      return statesById.get(channel.id)?.onAir === true;
    }

    if (filter === 'disabled') {
      return !channel.isEnabled;
    }

    if (filter === 'problems') {
      return hasProblems(channel);
    }

    return true;
  });
  const groups = groupedChannels(visibleChannels);
  const visibleIds = visibleChannels.map((channel) => channel.id);
  const selectedVisibleCount = visibleIds.filter((id) => selectedIds.has(id)).length;
  const allVisibleSelected = visibleIds.length > 0 && selectedVisibleCount === visibleIds.length;
  const someVisibleSelected = selectedVisibleCount > 0 && !allVisibleSelected;
  const selectedChannels = visibleChannels.filter((channel) => selectedIds.has(channel.id));

  const refreshAfterMutation = async (operation: () => Promise<void>) => {
    setMutationError(null);
    setMutating(true);

    try {
      await operation();
      setSelectedIds(new Set());
      query.refresh();
    } catch (error: unknown) {
      setMutationError(messageFromError(error));
    } finally {
      setMutating(false);
    }
  };

  const toggleAll = () => {
    setSelectedIds((current) => {
      const next = new Set(current);

      if (allVisibleSelected) {
        visibleIds.forEach((id) => next.delete(id));
      } else {
        visibleIds.forEach((id) => next.add(id));
      }

      return next;
    });
  };

  const toggleOne = (channelId: number) => {
    setSelectedIds((current) => {
      const next = new Set(current);

      if (next.has(channelId)) {
        next.delete(channelId);
      } else {
        next.add(channelId);
      }

      return next;
    });
  };

  const moveSelectedToGroup = () => {
    const group = window.prompt('Move selected channels to group', selectedChannels[0]?.group ?? '');

    if (group == null) {
      return;
    }

    void refreshAfterMutation(() => bulkMoveChannelsToGroup({
      channelIds: selectedChannels.map((channel) => channel.id),
      group
    }));
  };

  const renumberSelected = () => {
    const firstNumber = window.prompt('First channel number', selectedChannels[0]?.number ?? '1');

    if (firstNumber == null) {
      return;
    }

    const start = Number.parseFloat(firstNumber);

    if (!Number.isFinite(start)) {
      setMutationError('First channel number must be numeric');
      return;
    }

    void refreshAfterMutation(() => bulkRenumberChannels({
      channels: selectedChannels.map((channel, index) => ({
        id: channel.id,
        number: formatChannelNumber(start + index)
      }))
    }));
  };

  const deleteSelected = () => {
    if (!window.confirm(`Delete ${selectedChannels.length} selected channel${selectedChannels.length === 1 ? '' : 's'}?`)) {
      return;
    }

    void refreshAfterMutation(() => bulkDeleteChannels({ channelIds: selectedChannels.map((channel) => channel.id) }));
  };

  const changeFilter = (nextFilter: ChannelViewFilter) => {
    setFilter(nextFilter);
    setSelectedIds(new Set());
  };

  const deleteOne = (channel: ChannelSummary) => {
    if (!window.confirm(`Delete ${channel.name}?`)) {
      return;
    }

    void refreshAfterMutation(() => deleteChannel(channel.id));
  };

  // Blazor parity (ChannelEditor.razor "add" branch): a blank channel is created with these
  // defaults, then the editor opens for the rest of the fields. Distinct from "Add Channel",
  // which is the library-to-lineup ChannelBuilder flow (untouched here).
  const createBlankChannel = async () => {
    if (creatingBlank) {
      return;
    }

    setMutationError(null);
    setCreatingBlank(true);

    try {
      const ffmpegSettings = await getFfmpegSettings();
      const maxNumber = channels.reduce((max, channel) => {
        const parsed = Number.parseInt(channel.number.split('.')[0] ?? '', 10);
        return Number.isFinite(parsed) ? Math.max(max, parsed) : max;
      }, 0);

      // New channels default to the seeded logo-driven preset so one uploaded logo drives both the
      // guide listing and the on-screen bug (#67). A lookup failure must not block channel creation.
      let watermarkId: null | number = null;
      try {
        watermarkId = findLogoBugWatermark(await getWatermarks())?.id ?? null;
      } catch {
        watermarkId = null;
      }

      const body: CreateChannelRequest = {
        categories: null,
        fallbackFillerId: null,
        ffmpegProfileId: ffmpegSettings.defaultFFmpegProfileId,
        group: 'ErsatzTV',
        idleBehavior: 'StopOnDisconnect',
        isEnabled: true,
        logo: { path: null, contentType: null },
        mirrorSourceChannelId: null,
        musicVideoCreditsMode: 'None',
        musicVideoCreditsTemplate: null,
        name: 'New Channel',
        number: String(maxNumber + 1),
        playoutMode: 'Continuous',
        playoutOffset: null,
        playoutSource: 'Generated',
        preferredAudioLanguageCode: null,
        preferredAudioTitle: null,
        preferredSubtitleLanguageCode: null,
        showInEpg: true,
        slugSeconds: null,
        songVideoMode: 'Default',
        streamingMode: 'TransportStreamHybrid',
        streamSelector: null,
        streamSelectorMode: 'Default',
        subtitleMode: 'None',
        transcodeMode: 'OnDemand',
        watermarkId
      };

      const created = await createChannel(body);
      navigateToPath(`/app/edit-channel/${created.id}`);
    } catch (error: unknown) {
      setMutationError(messageFromError(error));
    } finally {
      setCreatingBlank(false);
    }
  };

  if (channels.length === 0) {
    return (
      <ChannelsEmptyState
        creatingBlank={creatingBlank}
        mutationError={mutationError}
        onCreateBlank={() => void createBlankChannel()}
      />
    );
  }

  return (
    <div className="ctv-channels-screen">
      <div className="ctv-channels-actionbar">
        {selectedChannels.length > 0 ? (
          <>
            <span className="ctv-channels-selected"><code>{selectedChannels.length}</code> selected</span>
            <span className="ctv-topbar-divider" />
            <Button disabled={mutating} onClick={renumberSelected} size="sm" startIcon={<Hash aria-hidden="true" size={14} />} variant="secondary">Renumber</Button>
            <Button disabled={mutating} onClick={moveSelectedToGroup} size="sm" startIcon={<FolderInput aria-hidden="true" size={14} />} variant="secondary">Move to group</Button>
            <Button disabled={mutating} onClick={deleteSelected} size="sm" startIcon={<Trash2 aria-hidden="true" size={14} />} variant="danger">Delete selected</Button>
            <span className="ctv-channels-spacer" />
            <Button disabled={mutating} onClick={() => setSelectedIds(new Set())} size="sm" variant="ghost">Clear</Button>
          </>
        ) : (
          <>
            <div className="ctv-segmented" role="group" aria-label="Channel view">
              <button type="button" aria-pressed={filter === 'all'} onClick={() => changeFilter('all')}>All <code>{channels.length}</code></button>
              <button type="button" aria-pressed={filter === 'onair'} onClick={() => changeFilter('onair')}>On air <code>{onAirCount}</code></button>
              <button type="button" aria-pressed={filter === 'disabled'} onClick={() => changeFilter('disabled')}>Disabled <code>{disabledCount}</code></button>
              <button type="button" aria-pressed={filter === 'problems'} onClick={() => changeFilter('problems')}>Problems <code>{problemsCount}</code></button>
            </div>
            <span className="ctv-channels-spacer" />
            <span className="ctv-channels-live"><StatusDot status="live" size={7} /><code>{onAirCount}</code> on air</span>
            <Button
              disabled={creatingBlank}
              loading={creatingBlank}
              onClick={() => void createBlankChannel()}
              size="sm"
              startIcon={<Plus aria-hidden="true" size={14} />}
              variant="secondary"
            >
              New blank channel
            </Button>
            <Button size="sm" startIcon={<Plus aria-hidden="true" size={14} />} onClick={() => navigateToPath('/app/new-channel')}>Add Channel</Button>
          </>
        )}
      </div>

      {mutationError && (
        <div className="ctv-channels-error" role="alert">
          <TriangleAlert aria-hidden="true" size={15} />
          <span>{mutationError}</span>
        </div>
      )}

      <div className="ctv-channels-table-frame">
        <div className="ctv-channels-table-scroll">
          <table className="ctv-channels-table" aria-label="Channels lineup">
            <thead>
              <tr>
                <th className="ctv-channel-check"><Checkbox checked={allVisibleSelected} indeterminate={someVisibleSelected} label="Select visible channels" onChange={toggleAll} /></th>
                <th>No.</th>
                <th>Channel</th>
                <th>Now playing</th>
                <th>Streaming</th>
                <th>FFmpeg</th>
                <th aria-label="Actions" />
              </tr>
            </thead>
            <tbody>
              {groups.map(({ group, rows }) => (
                <ChannelGroupRows
                  channels={rows}
                  group={group}
                  key={group}
                  mutating={mutating}
                  onDelete={deleteOne}
                  onPreview={setPreviewChannelId}
                  onToggle={toggleOne}
                  selectedIds={selectedIds}
                  statesById={statesById}
                />
              ))}
            </tbody>
          </table>
        </div>
        <div className="ctv-channels-footer">
          <span>{visibleChannels.length} of {channels.length} channels</span>
          <span><code>{groups.length}</code> groups</span>
        </div>
      </div>

      {previewChannelId !== null &&
        (() => {
          const previewChannel = channels.find((channel) => channel.id === previewChannelId);
          if (!previewChannel) {
            return null;
          }
          return (
            <ChannelPreviewPanel
              channel={{
                ...previewChannel,
                preview: {
                  ...previewChannel.preview,
                  // The generated ChannelSummary types `availability` as a bare `string` (see
                  // web/src/api/channels.ts); narrow it once here so ChannelPreviewPanel's own
                  // comparisons are checked against the real ChannelPreviewAvailability union.
                  availability: previewChannel.preview.availability as ChannelPreviewAvailability
                }
              }}
              nowPlaying={statesById.get(previewChannelId)?.nowPlaying ?? null}
              onClose={() => setPreviewChannelId(null)}
              open
            />
          );
        })()}
    </div>
  );
}

function ChannelGroupRows({
  channels,
  group,
  mutating,
  onDelete,
  onPreview,
  onToggle,
  selectedIds,
  statesById
}: {
  channels: ChannelSummary[];
  group: string;
  mutating: boolean;
  onDelete: (channel: ChannelSummary) => void;
  onPreview: (channelId: number) => void;
  onToggle: (channelId: number) => void;
  selectedIds: Set<number>;
  statesById: Map<number, ChannelState>;
}) {
  return (
    <>
      <tr className="ctv-channel-group-row">
        <td colSpan={7}>
          <span><Folder aria-hidden="true" size={13} />{group}<code>{channels.length}</code></span>
        </td>
      </tr>
      {channels.map((channel) => (
        <ChannelTableRow
          channel={channel}
          key={channel.id}
          mutating={mutating}
          onDelete={onDelete}
          onPreview={onPreview}
          onToggle={onToggle}
          selected={selectedIds.has(channel.id)}
          state={statesById.get(channel.id) ?? null}
        />
      ))}
    </>
  );
}

function formatChannelNumber(value: number): string {
  return Number.isInteger(value) ? `${value}` : value.toFixed(2).replace(/0+$/, '').replace(/\.$/, '');
}

function ChannelTableRow({
  channel,
  mutating,
  onDelete,
  onPreview,
  onToggle,
  selected,
  state
}: {
  channel: ChannelSummary;
  mutating: boolean;
  onDelete: (channel: ChannelSummary) => void;
  onPreview: (channelId: number) => void;
  onToggle: (channelId: number) => void;
  selected: boolean;
  state: ChannelState | null;
}) {
  const live = state?.onAir === true;
  const dim = !channel.isEnabled;
  const progress = state ? progressFromChannelState(state) : null;
  const streamingTone = streamingModeBadgeTones[channel.streamingMode] ?? 'neutral';
  // Narrow once: ChannelSummary types `preview.availability` as a bare `string` (see
  // web/src/api/channels.ts), so comparing the union-typed local below against a mistyped literal
  // (e.g. 'UnavailableX') is a compile error, where comparing the raw string field would not be.
  const previewAvailability = channel.preview.availability as ChannelPreviewAvailability;

  return (
    <tr className={`${live ? 'ctv-channel-row-live ' : ''}${selected ? 'ctv-channel-row-selected ' : ''}${dim ? 'ctv-channel-row-dim' : ''}`}>
      <td className="ctv-channel-check">
        <Checkbox checked={selected} label={`Select ${channel.name}`} onChange={() => onToggle(channel.id)} />
      </td>
      <td className="ctv-channel-number">
        <span>{live && <StatusDot status="live" size={7} />}<code>{channel.number}</code></span>
      </td>
      <td>
        <div className="ctv-channel-identity">
          <ChannelLogo name={channel.name} size={34} src={channel.logo} />
          <div>
            <strong>{channel.name}</strong>
            <span className="ctv-channel-markers">
              {!channel.isEnabled && <Marker title="Disabled">D</Marker>}
              {!channel.showInEpg && <Marker title="Hidden from EPG">H</Marker>}
              {/*
                Spelled out rather than compressed to a letter marker like D/H: those flag states the
                user chose deliberately, whereas this one is a fault they need to notice and act on.
                A tooltip-only glyph would be invisible on touch and easy to miss in a long lineup.
              */}
              {(channel.health?.faults ?? []).map((fault) => (
                <Badge key={fault} tone="error">{FAULT_LABELS[fault] ?? fault}</Badge>
              ))}
              {/*
                Creation-provenance marker. Rendered only for AutoTuned: its absence means user-created
                (or a legacy row predating the origin column, which reads as 'Unknown' — we never
                back-fill provenance, so we don't assert 'user-created' about those).
              */}
              {channel.origin === 'AutoTuned' && (
                <span title="Created by Auto-Tune from a channel lineup">
                  <Badge tone="neutral">Auto-tuned</Badge>
                </span>
              )}
            </span>
            <small>{channel.language || 'Language unset'}</small>
          </div>
        </div>
      </td>
      <td className="ctv-channel-now">
        {state?.nowPlaying ? (
          <div>
            <span>{state.nowPlaying.title}</span>
            <ProgressBar height={3} value={progress} />
          </div>
        ) : (
          <span>{channel.isEnabled ? 'Idle' : 'Off air'}</span>
        )}
      </td>
      <td><Badge tone={streamingTone}>{channel.streamingMode}</Badge></td>
      <td className="ctv-channel-ffmpeg">{channel.ffmpegProfile || 'Unassigned'}</td>
      <td>
        <div className="ctv-channel-actions">
          <IconButton
            disabled={previewAvailability === 'Unavailable'}
            onClick={() => onPreview(channel.id)}
            size="sm"
            title={
              previewAvailability === 'Unavailable'
                ? channel.preview.unavailableReason
                  ? `Preview unavailable for ${channel.name} — ${channel.preview.unavailableReason}`
                  : `Preview unavailable for ${channel.name}`
                : `Preview ${channel.name}`
            }
          >
            <Play aria-hidden="true" size={15} />
          </IconButton>
          <IconButton onClick={() => navigateToPath(`/app/edit-channel/${channel.id}`)} size="sm" title={`Edit ${channel.name}`}>
            <Pencil aria-hidden="true" size={15} />
          </IconButton>
          <IconButton
            onClick={() => navigateToPath(`/app/troubleshooting/playback?channel=${channel.id}`)}
            size="sm"
            title={`Troubleshoot ${channel.name}`}
          >
            <Stethoscope aria-hidden="true" size={15} />
          </IconButton>
          <IconButton disabled={mutating} onClick={() => onDelete(channel)} size="sm" title={`Delete ${channel.name}`}>
            <Trash2 aria-hidden="true" size={15} />
          </IconButton>
        </div>
      </td>
    </tr>
  );
}
