import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Room, SyncState } from 'matrix-js-sdk';
import type { BotPreset } from './catalog';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useSyncState } from '../../hooks/useSyncState';
import {
  getCanonicalAliasOrRoomId,
  getCanonicalAliasRoomId,
  isRoomAlias,
} from '../../utils/matrix';
import { getChannelsSpacePath } from '../../pages/pathUtils';
import type { MatrixToRoom } from '../../plugins/matrix-to';
import { useBotWidgetEmbed } from './useBotWidgetEmbed';
import * as css from './BotWidgetMount.css';

// Anti-flicker debounce — same rationale as `SyncIndicator`'s
// `PROGRESS_FADE_IN_DELAY_MS`. On a warm/cached widget the iframe can
// boot in <100ms; without the delay the bar would briefly appear and
// fade out, reading as a flash. 100ms is just above the typical instant-
// load envelope yet below the perception threshold for «I clicked and
// nothing happened».
const LOADING_BAR_FADE_IN_DELAY_MS = 100;

// Fallback hide deadline if `animationiteration` never fires — covers
// any compositor freeze that drops the event (e.g. tab backgrounded
// mid-cycle and just foregrounded). 2000ms gives ~400ms slack over the
// 1.6s slide-cycle: a fast 60Hz display fires iteration ~16-32ms after
// cycle end, but a backgrounded compositor can be 100+ms late. The
// reduced-motion path skips this branch entirely.
const LOADING_BAR_HIDE_FALLBACK_MS = 2000;

// Renders the iframe-mount target and wires up `useBotWidgetEmbed` to
// drive the iframe lifecycle. Renamed from BotWidgetHost in the BotShell
// refactor — «Host» was overloaded with BotExperienceHost (page-level
// dispatcher); «Mount» reads as the literal DOM mount point this is.
type BotWidgetMountProps = {
  preset: BotPreset;
  room: Room;
  onError: () => void;
};

export function BotWidgetMount({ preset, room, onError }: BotWidgetMountProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const navigate = useNavigate();
  const mx = useMatrixClient();

  // Generic «navigate cinny to a matrix.to room/alias». Bot-agnostic: any
  // widget that posts `{action: 'open-matrix-to', data: {url}}` on the
  // `io.vojo.bot-widget` side-channel reaches this. The embed has already
  // validated the URL via `parseMatrixToRoom` so `target` is well-formed.
  // For an alias we resolve to the canonical room id first — the channels
  // path expects an id-or-alias either way, but joined-room lookup needs
  // the id form for the via-server hint to be effective. `viaServers` are
  // currently dropped (the channels view doesn't propagate them); add a
  // dedicated «join-via» path if a future widget needs to surface a room
  // the user hasn't joined yet.
  const handleOpenMatrixToRoom = useCallback(
    (target: MatrixToRoom) => {
      const { roomIdOrAlias } = target;
      const idOrAlias = isRoomAlias(roomIdOrAlias)
        ? getCanonicalAliasRoomId(mx, roomIdOrAlias) ?? roomIdOrAlias
        : roomIdOrAlias;
      const canonical = getCanonicalAliasOrRoomId(mx, idOrAlias);
      navigate(getChannelsSpacePath(canonical));
    },
    [mx, navigate]
  );

  const { ready } = useBotWidgetEmbed({
    containerRef,
    preset,
    room,
    onError,
    onOpenMatrixToRoom: handleOpenMatrixToRoom,
  });

  // Track Matrix sync state so the bot loading bar yields to the global
  // SyncIndicator when the connection is unhealthy. Without this, on a
  // dropped network the user would see TWO sweeping bars at once — the
  // bot bar at top stuck in «still loading» plus the SyncIndicator at
  // bottom in transient/error state. The bottom bar is the canonical
  // connection-state surface; the top one defers. Reuses `mx` from the
  // navigate-callback block above — single hook call per render.
  const [syncState, setSyncState] = useState<SyncState | null>(() => mx.getSyncState());
  useSyncState(
    mx,
    useCallback((state: SyncState) => {
      setSyncState(state);
    }, [])
  );
  useEffect(() => {
    // Close the render-to-effect gap (same pattern as SyncIndicator) so
    // a Sync event fired between useState init and listener attach
    // doesn't get dropped.
    setSyncState(mx.getSyncState());
  }, [mx]);

  // Healthy = SyncIndicator's «hidden» branch (`Syncing` or `Stopped`).
  // Anything else (`Reconnecting`, `Catchup`, `Error`, initial null)
  // means the bottom bar will be visible, so we should not also show
  // ours. Mirrors `stateToVisual` in SyncIndicator.tsx.
  const syncHealthy = syncState === SyncState.Syncing || syncState === SyncState.Stopped;

  // Three logical phases for the loading bar:
  //   - hidden  (visible=false, pendingHide=false) → initial / post-hide
  //   - visible (visible=true,  pendingHide=false) → animating actively
  //   - closing (visible=true,  pendingHide=true)  → ready fired mid-
  //                                                  cycle, waiting for
  //                                                  animationiteration
  //                                                  to land at the
  //                                                  cycle boundary
  //                                                  before snapping
  //                                                  invisible
  // The closing → hidden transition is driven by the iteration event so
  // the user always sees a complete left-to-right sweep and never a
  // mid-stroke cut. A fallback timer covers the case where the event
  // can't fire (compositor freeze on a backgrounded tab). Reduced-motion
  // and sync-unhealthy paths skip cycle-complete entirely — for the
  // first there's no animation cycle to honour, for the second the
  // bottom SyncIndicator should take over without UI overlap.
  const [visible, setVisible] = useState(false);
  const [pendingHide, setPendingHide] = useState(false);

  // Reasons the bar should NOT be shown:
  //   - 'ready' — widget loaded normally, hide gracefully (cycle-complete)
  //   - 'sync'  — connection unhealthy, snap-hide so the SyncIndicator's
  //               bar is the only visible chrome (no double-bar UX)
  //   - null    — bar should be visible
  // Order matters: `ready` takes precedence so a successfully-loaded
  // widget that then drops its connection still gets the polished cycle
  // hide on the original ready event rather than being re-classified as
  // a sync-issue snap.
  let hideReason: 'ready' | 'sync' | null = null;
  if (ready) hideReason = 'ready';
  else if (!syncHealthy) hideReason = 'sync';

  useEffect(() => {
    if (hideReason !== null) {
      // Bar wasn't shown yet — cached fast load (or a handshake that
      // landed during the debounce window) won the race. Nothing to do;
      // bar stays hidden, no flash.
      if (!visible) return undefined;
      // Sync-issue snap: jump straight to invisible so the bottom
      // SyncIndicator can take over without two bars overlapping.
      // Reduced-motion: animation is off (no iterations ever land), so
      // parking a static stripe for ~2s isn't graceful, just stuck.
      if (hideReason === 'sync' || window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
        setVisible(false);
        setPendingHide(false);
        return undefined;
      }
      // hideReason === 'ready' with motion enabled — bar is mid-sweep.
      // Defer the actual hide to the next cycle boundary. Fallback
      // force-hides if the event never lands.
      setPendingHide(true);
      const fallback = setTimeout(() => {
        setVisible(false);
        setPendingHide(false);
      }, LOADING_BAR_HIDE_FALLBACK_MS);
      return () => clearTimeout(fallback);
    }
    // Bar should show — clear any leftover hide request from a previous
    // ready-then-not-ready blip and (if not yet visible) schedule the
    // debounced fade-in.
    setPendingHide(false);
    if (visible) return undefined;
    const fadeIn = setTimeout(() => setVisible(true), LOADING_BAR_FADE_IN_DELAY_MS);
    return () => clearTimeout(fadeIn);
  }, [hideReason, visible]);

  const handleIteration = () => {
    if (pendingHide) {
      setVisible(false);
      setPendingHide(false);
    }
  };

  // Loading bar stays mounted; opacity carries the visible/hidden
  // crossfade. animationPlayState is paused while invisible so the
  // compositor doesn't keep sweeping a 0-alpha element forever.
  // pointer-events: none on the root so it never intercepts iframe
  // clicks even mid-fade.
  return (
    <>
      <div ref={containerRef} className={css.FrameMount} />
      <div className={css.LoadingBarRoot} aria-hidden>
        <div
          className={css.LoadingBar}
          style={{
            opacity: visible ? 1 : 0,
            animationPlayState: visible ? 'running' : 'paused',
          }}
          onAnimationIteration={handleIteration}
        />
      </div>
    </>
  );
}
