import React, { Component, createRef, type ReactNode } from 'react';
import { AccessibilityInfo, AppState, Dimensions, Keyboard, StyleSheet, View } from 'react-native';

import { createAgentController } from './agent/compose.ts';
import { SYSTEM_CLOCK } from './actions/settle.ts';
import { AgentModeController, type AgentModeSnapshot } from './agent-mode/controller.ts';
import { CursorController, type CursorSnapshot } from './agent-mode/cursor.ts';
import { HAPTICS_ADAPTER, warnByHaptics } from './agent-mode/haptics.ts';
import { SpotlightController, type SpotlightSnapshot } from './agent-mode/spotlight-controller.ts';
import { SYSTEM_TIMERS } from './agent-mode/timers.ts';
import { readAgentModeEvent } from './agent-mode/frames.ts';
import type { AgentModePhase } from './agent-mode/state.ts';
import { KEEP_AWAKE_ADAPTER, KeepAwake } from './agent-mode/keep-awake.ts';
import { LayerStacking } from './agent-mode/layer-stacking.ts';
import { AgentLayer } from './agent-mode/ui/AgentLayer.tsx';
import { AgentLayerHost } from './agent-mode/ui/AgentLayerHost.tsx';
import { BackendTransport, type WidgetProof } from './backend/backend-transport.ts';
import type { BlarIdentity } from './backend/identity.ts';
import type { MessageMetadata } from './backend/message-metadata.ts';
import { Blar, type ChatControls } from './blar.ts';
import type { AgentController } from './agent/controller.ts';
import type { NavigationRefLike } from './actions/navigation-driver.ts';
import { ChatCoordinator, type ChatBackend, type ChatSnapshot } from './chat/chat-coordinator.ts';
import { ChatLayer } from './chat/ChatLayer.tsx';
import { type ChatOptions, resolveChatOptions, type ResolvedChatOptions } from './chat/options.ts';
import { currentRootFiber } from './fiber/locate.ts';
import type { Fiber } from './fiber/types.ts';
import { visibleViewport } from './host/visible-area.ts';
import { AgentOverlay } from './overlay/AgentOverlay.tsx';
import { CAPTURE_BACKEND } from './screenshot/screenshotter.ts';
import { countOpenModals } from './snapshot/modals.ts';
import { SdkErrorBoundary } from './telemetry/SdkErrorBoundary.tsx';
import { resolveConnectionOptions, type ConnectionConfig } from './transport/connection-options.ts';
import { RelayTransport } from './transport/relay-transport.ts';
import type { AgentTransport } from './transport/transport.ts';
import { type Language, resolveLanguage, resolveStrings, type UiStringOverrides, type UiStrings } from './ui/strings.ts';

export interface BlarProviderProps {
  children: ReactNode;
  publishableKey?: string;
  apiBaseUrl?: string;
  user?: BlarIdentity | null;
  relayUrl?: string;
  navigationRef?: NavigationRefLike | null;
  language?: Language;
  strings?: UiStringOverrides;
  chat?: ChatOptions;
  enabled?: boolean;
  debug?: boolean;
}

interface BlarProviderState {
  spotlight: SpotlightSnapshot;
  chat: ChatSnapshot | null;
  agentMode: AgentModeSnapshot | null;
  cursor: CursorSnapshot;
  reduceMotion: boolean;
  layerGeneration: number;
}

interface WithReactInternals {
  _reactInternals?: Fiber;
}

export class BlarProvider extends Component<BlarProviderProps, BlarProviderState> {
  private connection: ConnectionConfig = { kind: 'disabled' };

  private readonly cursor = new CursorController({
    clock: SYSTEM_CLOCK,
    isForeground: () => AppState.currentState === 'active',
  });

  private readonly spotlight = new SpotlightController({
    timers: SYSTEM_TIMERS,
    isForeground: () => AppState.currentState === 'active',
    stopRun: () => this.agentMode?.stop(),
    alert: () => warnByHaptics(HAPTICS_ADAPTER),
  });

  override state: BlarProviderState = {
    spotlight: this.spotlight.getSnapshot(),
    chat: null,
    agentMode: null,
    cursor: this.cursor.getSnapshot(),
    reduceMotion: false,
    layerGeneration: 0,
  };

  private readonly contentRef = createRef<View>();

  private controller: AgentController | null = null;

  private agentMode: AgentModeController | null = null;

  private transport: AgentTransport | null = null;

  private backend: BackendTransport | null = null;

  private coordinator: ChatCoordinator | null = null;

  private chatControls: ChatControls | null = null;

  private shownNudge: number | null = null;

  private readonly releases: Array<() => void> = [];

  private stackingCheckQueued = false;

  private loggedPhase: AgentModePhase = 'idle';

  private readonly stacking = new LayerStacking(() => this.countHostModals());

  private readonly attachCursor = (): void => this.cursor.attach();

  private readonly detachCursor = (): void => this.cursor.detach();

  override componentDidMount(): void {
    if (this.props.enabled !== false) {
      this.startSdk();
    }
  }

  override componentDidUpdate(previous: BlarProviderProps): void {
    const enabled = this.props.enabled !== false;
    if (enabled !== (previous.enabled !== false)) {
      if (enabled) {
        this.startSdk();
      } else {
        this.stopSdk();
        this.setState({ chat: null, agentMode: null, spotlight: this.spotlight.getSnapshot(), cursor: this.cursor.getSnapshot() });
      }
      return;
    }
    if (this.backend === null || previous.user?.externalId === this.props.user?.externalId) {
      return;
    }
    if (this.props.user === undefined || this.props.user === null) {
      void Blar.logout();
      return;
    }
    void Blar.setUser(this.props.user);
  }

  override componentWillUnmount(): void {
    this.stopSdk();
  }

  private startSdk(): void {
    this.connection = resolveConnectionOptions(this.props, typeof __DEV__ !== 'undefined' && __DEV__);
    const transport = this.createTransport();
    if (transport === null) {
      this.debug('SDK disabled: provide a valid publishableKey and apiBaseUrl, or an explicit relayUrl');
      return;
    }
    this.watchMotionPreference();
    this.releases.push(this.cursor.subscribe((cursor) => this.setState({ cursor })));
    this.releases.push(this.spotlight.subscribe((spotlight) => this.setState({ spotlight })));
    this.watchForeground();
    const agentMode = this.createAgentMode();
    this.transport = transport;
    this.createChat();
    this.controller = createAgentController({
      transport,
      getAnchorFiber: () => (this as WithReactInternals)._reactInternals ?? null,
      getViewport: () => visibleViewport(Dimensions.get('window'), Keyboard.metrics()?.height ?? 0),
      getNavigationRef: () => this.props.navigationRef ?? null,
      getCaptureTarget: () => this.contentRef.current,
      reading: this.spotlight,
      cursor: this.cursor,
      confirmation: this.spotlight,
      stage: agentMode,
      onCommit: () => this.queueStackingCheck(),
      onSettled: () => this.keepLayerOnTop(),
    });
    this.controller.start();
    this.debug(`screenshots ${CAPTURE_BACKEND?.name ?? 'unavailable'}, haptics ${HAPTICS_ADAPTER?.name ?? 'unavailable'}`);
    this.attachBackend();
  }

  private stopSdk(): void {
    if (this.backend !== null) {
      Blar.detach(this.backend);
      this.backend = null;
    }
    if (this.chatControls !== null) {
      Blar.detachChat(this.chatControls);
      this.chatControls = null;
    }
    this.releases.splice(0).forEach((release) => release());
    this.coordinator = null;
    this.controller?.stop();
    this.controller = null;
    this.agentMode?.dispose();
    this.agentMode = null;
    this.transport = null;
    this.spotlight.clear();
    this.loggedPhase = 'idle';
    this.shownNudge = null;
  }

  override render(): React.JSX.Element {
    const strings = resolveStrings(this.props.language ?? 'es', this.props.strings);
    const phase = this.state.agentMode?.phase ?? 'idle';
    return (
      <View style={styles.root} collapsable={false} onTouchStart={() => this.backend?.noteInteraction()}>
        <View ref={this.contentRef} style={styles.root} collapsable={false}>
          {this.props.children}
        </View>
        <SdkErrorBoundary context="BlarProvider.overlay">
          <AgentOverlay>
            {this.renderChat(strings)}
            <AgentLayerHost visible={phase !== 'idle'} generation={this.state.layerGeneration} onRequestClose={() => this.agentMode?.stop()}>
              {this.renderAgentLayer(strings)}
            </AgentLayerHost>
          </AgentOverlay>
        </SdkErrorBoundary>
      </View>
    );
  }

  private renderAgentLayer(strings: UiStrings): React.JSX.Element | null {
    const { agentMode } = this.state;
    if (agentMode === null || agentMode.phase === 'idle') {
      return null;
    }
    return (
      <AgentLayer
        snapshot={agentMode}
        strings={strings}
        color={this.chatOptions().primaryColor}
        spotlight={this.state.spotlight}
        spotlightActions={this.spotlight}
        cursor={this.state.cursor}
        reduceMotion={this.state.reduceMotion}
        onCursorAttach={this.attachCursor}
        onCursorDetach={this.detachCursor}
        onStop={() => this.agentMode?.stop()}
      />
    );
  }

  private renderChat(strings: UiStrings): React.JSX.Element | null {
    const { chat } = this.state;
    const backend = this.backend;
    const coordinator = this.coordinator;
    const connection = this.connection;
    if (chat === null || backend === null || coordinator === null || connection.kind !== 'backend') {
      return null;
    }
    return (
      <ChatLayer
        snapshot={chat}
        options={this.chatOptions()}
        strings={strings}
        language={resolveLanguage(this.props.language)}
        publishableKey={connection.publishableKey}
        apiBaseUrl={connection.apiBaseUrl}
        tabId={backend.getTabId()}
        identity={backend.getIdentity()}
        getMetadata={(options) => this.readChatMetadata(options)}
        getProof={() => this.readChatProof()}
        onOpen={() => coordinator.open()}
        onClose={() => coordinator.close()}
        onSessionChanged={(sessionId) => this.followWidgetSession(sessionId)}
        onAcceptNudge={() => void coordinator.acceptNudge()}
        onDeclineNudge={() => coordinator.declineNudge()}
        onSnoozeNudge={() => coordinator.snoozeNudge()}
        log={this.props.debug === true ? (message) => console.log(`[blar] ${message}`) : undefined}
      />
    );
  }

  private chatOptions(): ResolvedChatOptions {
    return resolveChatOptions(this.props.chat);
  }

  private watchForeground(): void {
    const subscription = AppState.addEventListener('change', (state) => {
      if (state !== 'active') {
        this.spotlight.backgrounded();
      }
    });
    this.releases.push(() => subscription.remove());
  }

  private watchMotionPreference(): void {
    void AccessibilityInfo.isReduceMotionEnabled().then((reduceMotion) => this.setState({ reduceMotion }));
    const subscription = AccessibilityInfo.addEventListener('reduceMotionChanged', (reduceMotion) => this.setState({ reduceMotion }));
    this.releases.push(() => subscription.remove());
  }

  private createAgentMode(): AgentModeController {
    const agentMode = new AgentModeController(
      {
        sendCancel: (reason) => this.transport?.send({ type: 'agent_cancel', reason }),
        onActive: () => {
          this.cursor.beginSession();
          this.stacking.baseline();
          this.coordinator?.agentEngaged();
        },
        onIdle: () => {
          this.cursor.endSession();
          this.spotlight.clear();
          this.coordinator?.agentReleased();
        },
      },
      new KeepAwake(KEEP_AWAKE_ADAPTER, (message) => this.debug(message)),
      this.backend?.getRunMemory(),
    );
    this.agentMode = agentMode;
    this.setState({ agentMode: agentMode.getSnapshot() });
    this.releases.push(
      agentMode.subscribe((snapshot) => {
        if (snapshot.phase !== this.loggedPhase) {
          this.loggedPhase = snapshot.phase;
          this.debug(`agent mode ${snapshot.phase}`);
        }
        this.setState({ agentMode: snapshot });
      }),
    );
    return agentMode;
  }

  private createTransport(): AgentTransport | null {
    const connection = this.connection;
    if (connection.kind === 'disabled') {
      return null;
    }
    if (connection.kind === 'relay') {
      return new RelayTransport(connection.relayUrl);
    }
    const backend = new BackendTransport({
      publishableKey: connection.publishableKey,
      apiBaseUrl: connection.apiBaseUrl,
      user: this.props.user ?? Blar.consumePendingUser(),
      log: this.props.debug === true ? (message) => console.log(`[blar] ${message}`) : undefined,
      onSocketState: (state) => {
        if (state === 'failed') {
          this.agentMode?.socketFailed();
        }
      },
      onConnecting: (sessionId) => this.debug(`agent resume ${this.agentMode?.connecting(sessionId) ?? 'none'} session=${sessionId}`),
      onSessionChanged: () => this.agentMode?.endClosedRun(),
    });
    this.backend = backend;
    this.releases.push(
      backend.onFrame((frame) => {
        const event = readAgentModeEvent(frame);
        if (event !== null) {
          this.agentMode?.handleEvent(event);
        }
      }),
    );
    return backend;
  }

  private createChat(): void {
    const backend = this.backend;
    if (backend === null || !this.chatOptions().enabled) {
      return;
    }
    const adapter: ChatBackend = {
      getSessionId: () => backend.getState().sessionId,
      openNudge: () => backend.openNudge(),
      dismissNudge: (milestoneMessageId) => backend.dismissNudge(milestoneMessageId),
      snoozeNudge: (milestoneMessageId) => backend.snoozeNudge(milestoneMessageId),
      switchSession: (sessionId) => backend.switchSession(sessionId),
    };
    const coordinator = new ChatCoordinator(adapter, { onSessionReplaced: () => this.agentMode?.endClosedRun() });
    this.coordinator = coordinator;
    this.setState({ chat: coordinator.getSnapshot() });
    this.releases.push(
      coordinator.subscribe((chat) => {
        this.noteNudge(chat);
        this.setState({ chat });
      }),
    );
    this.releases.push(backend.onFrame((frame) => coordinator.handleFrame(frame)));
    this.releases.push(backend.onIdentityChange(() => coordinator.reset()));
    this.chatControls = {
      open: () => coordinator.open(),
      close: () => coordinator.close(),
      getUnread: () => coordinator.getSnapshot().chat.unread,
      isOpen: () => coordinator.getSnapshot().chat.mode === 'open',
    };
    Blar.attachChat(this.chatControls);
  }

  private attachBackend(): void {
    const backend = this.backend;
    const controller = this.controller;
    if (backend === null || controller === null) {
      return;
    }
    Blar.attach(backend, (options) => controller.readMessageMetadata({ ...options, tabId: backend.getTabId() }));
  }

  private queueStackingCheck(): void {
    if (this.stackingCheckQueued || (this.agentMode?.getSnapshot().phase ?? 'idle') === 'idle') {
      return;
    }
    this.stackingCheckQueued = true;
    requestAnimationFrame(() => {
      this.stackingCheckQueued = false;
      this.keepLayerOnTop();
    });
  }

  private keepLayerOnTop(): void {
    if ((this.agentMode?.getSnapshot().phase ?? 'idle') === 'idle' || !this.stacking.coveredByNewModal()) {
      return;
    }
    this.debug('host modal opened above the agent layer, remounting it');
    this.setState(({ layerGeneration }) => ({ layerGeneration: layerGeneration + 1 }));
  }

  private countHostModals(): number {
    const anchor = (this as WithReactInternals)._reactInternals ?? null;
    const root = anchor === null ? null : currentRootFiber(anchor);
    return root === null ? 0 : countOpenModals(root);
  }

  private noteNudge(chat: ChatSnapshot): void {
    const milestone = chat.nudge?.milestoneMessageId ?? null;
    if (milestone !== this.shownNudge) {
      this.shownNudge = milestone;
      this.debug(milestone === null ? 'nudge hidden' : `nudge shown milestone=${milestone}`);
    }
  }

  private debug(message: string): void {
    if (this.props.debug === true) {
      console.log(`[blar] ${message}`);
    }
  }

  private followWidgetSession(sessionId: number | null): void {
    const backend = this.backend;
    if (backend === null || backend.getState().sessionId === sessionId) {
      return;
    }
    if (sessionId === null) {
      this.agentMode?.endClosedRun();
      backend.leaveSession();
      return;
    }
    void backend.switchSession(sessionId);
  }

  private readChatMetadata(options: { includeTextDom: boolean }): Promise<MessageMetadata | null> {
    return Blar.getMessageMetadata(options);
  }

  private async readChatProof(): Promise<WidgetProof | null> {
    return this.backend?.readWidgetProof() ?? null;
  }

}

const styles = StyleSheet.create({
  root: { flex: 1 },
});
