import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { Linking, StyleSheet, Text, View } from 'react-native';
import type { WebViewMessageEvent } from 'react-native-webview';

import type { WidgetProof } from '../backend/backend-transport.ts';
import type { MessageMetadata } from '../backend/message-metadata.ts';
import type { UnknownRecord } from '../fiber/narrow.ts';
import { buildEventScript, buildResolveScript, parsePageMessage, type PageMessage } from './bridge.ts';
import type { SessionRequest } from './chat-coordinator.ts';
import { type ChatNavigation, leavesChat } from './navigation.ts';
import { buildWidgetShell, type WidgetShellOptions } from './widget-shell.ts';
import { loadWebView, type WebViewHandle } from './webview.ts';

export interface ChatViewProps {
  shell: WidgetShellOptions;
  unavailableText: string;
  getMetadata: (options: { includeTextDom: boolean }) => Promise<MessageMetadata | null>;
  getProof: () => Promise<WidgetProof | null>;
  onClosed: () => void;
  onSessionChanged: (sessionId: number | null) => void;
  open: boolean;
  sessionRequest: SessionRequest | null;
  log?: (message: string) => void;
}

const WebView = loadWebView();

export function ChatView({ shell, unavailableText, getMetadata, getProof, onClosed, onSessionChanged, open, sessionRequest, log }: ChatViewProps): React.JSX.Element {
  const handle = useRef<WebViewHandle | null>(null);
  const html = useMemo(() => buildWidgetShell(shell), [shell]);

  const answer = useCallback((id: string, value: unknown, error: string | null) => {
    handle.current?.injectJavaScript(buildResolveScript(id, value, error));
  }, []);

  useEffect(() => {
    if (open) {
      handle.current?.injectJavaScript(buildEventScript('open', null));
    }
  }, [open]);

  useEffect(() => {
    if (sessionRequest !== null) {
      handle.current?.injectJavaScript(buildEventScript('openSession', { sessionId: sessionRequest.sessionId }));
    }
  }, [sessionRequest]);

  const handleCall = useCallback(
    async (message: Extract<PageMessage, { kind: 'call' }>) => {
      try {
        const value = message.name === 'metadata' ? await getMetadata({ includeTextDom: readIncludeTextDom(message.args) }) : await getProof();
        answer(message.id, value, null);
      } catch (error) {
        answer(message.id, null, error instanceof Error ? error.message : String(error));
      }
    },
    [answer, getMetadata, getProof],
  );

  const onMessage = useCallback(
    (event: WebViewMessageEvent) => {
      const message = parsePageMessage(event.nativeEvent.data);
      if (message === null) {
        return;
      }
      if (message.kind === 'call') {
        void handleCall(message);
      } else if (message.kind === 'closed') {
        onClosed();
      } else if (message.kind === 'session') {
        onSessionChanged(message.sessionId);
      } else if (message.kind === 'log') {
        log?.(`webview ${message.level}: ${message.text}`);
      } else {
        log?.('webview ready');
      }
    },
    [handleCall, log, onClosed, onSessionChanged],
  );

  const onNavigation = useCallback(
    (navigation: ChatNavigation) => {
      if (!leavesChat(navigation, shell.apiBaseUrl)) {
        return true;
      }
      Linking.openURL(navigation.url).catch(() => log?.('chat link could not be opened'));
      return false;
    },
    [log, shell.apiBaseUrl],
  );

  if (WebView === null) {
    return (
      <View style={styles.fallback}>
        <Text style={styles.fallbackText}>{unavailableText}</Text>
      </View>
    );
  }
  return (
    <WebView
      ref={handle}
      style={styles.webview}
      originWhitelist={['*']}
      source={{ html, baseUrl: shell.apiBaseUrl }}
      javaScriptEnabled
      domStorageEnabled
      mixedContentMode="always"
      setSupportMultipleWindows={false}
      keyboardDisplayRequiresUserAction={false}
      // The sheet's KeyboardAvoidingView already lifts the chat above the keyboard. WKWebView would also scroll its
      // own page up to reveal the composer, pushing the widget out of the sheet; with scrolling off it resets that
      // offset (the message list is an overflow container and keeps scrolling). The form bar (^ v Done) is for
      // multi-field forms, not a one-field composer.
      scrollEnabled={false}
      hideKeyboardAccessoryView
      automaticallyAdjustContentInsets={false}
      overScrollMode="never"
      onMessage={onMessage}
      onShouldStartLoadWithRequest={onNavigation}
      onError={(event) => log?.(`webview load error: ${event.nativeEvent.description}`)}
    />
  );
}

function readIncludeTextDom(args: UnknownRecord): boolean {
  return args.includeTextDom === true;
}

const styles = StyleSheet.create({
  webview: { flex: 1, backgroundColor: 'transparent' },
  fallback: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 24 },
  fallbackText: { color: '#6b7280', textAlign: 'center' },
});
