import { useEffect } from 'react';
import { useSetAtom } from 'jotai';
import type { PluginListenerHandle } from '@capacitor/core';
import {
  shareTarget,
  sharedFilePathToUrl,
  type PendingShare,
  type SharedFile,
} from '../plugins/shareTarget';
import { pendingShareAtom, type PendingShareValue } from '../state/pendingShare';
import { isAndroidPlatform } from '../utils/capacitor';

// Convert a SharedFile (cached path + metadata) into a real File so it slots
// straight into RoomInput.handleFiles. The fetch() goes through the
// capacitor:// scheme handler which translates back to a direct cacheDir
// read — no network round-trip.
const hydrateSharedFile = async (item: SharedFile): Promise<File | null> => {
  try {
    const url = sharedFilePathToUrl(item.path);
    const res = await fetch(url);
    if (!res.ok) return null;
    const blob = await res.blob();
    // ContentResolver-reported mimeType is more accurate than the blob's
    // sniffed type for ambiguous cases (no extension, octet-stream).
    return new File([blob], item.name, {
      type: item.mimeType || blob.type || 'application/octet-stream',
    });
  } catch {
    return null;
  }
};

// Merge subject and text into a single composer string. ACTION_SEND sources
// like Gmail "Share thread" put the email subject in EXTRA_SUBJECT and the
// body in EXTRA_TEXT; sources like Chrome "Share page" sometimes send only
// EXTRA_SUBJECT (the page title) with no body. We treat them as one stream
// of text the user will edit before sending — no need to model them as two
// independent fields downstream.
const mergeSubjectAndText = (
  text: string | undefined,
  subject: string | undefined
): string | undefined => {
  if (text && subject && text !== subject) return `${subject}\n\n${text}`;
  return text || subject;
};

const hydrateShare = async (share: PendingShare): Promise<PendingShareValue | null> => {
  if (share.empty) return null;
  const fileResults = await Promise.all((share.items ?? []).map(hydrateSharedFile));
  const files = fileResults.filter((f): f is File => f !== null);
  const text = mergeSubjectAndText(share.text, share.subject);
  // Nothing useful to act on — drop. Possible when text+subject were both
  // empty AND every file failed to hydrate (cache eviction between native
  // capture and JS pickup, or a permission-denied content URI we never
  // managed to read).
  if (!text && files.length === 0) return null;
  return {
    text,
    files,
    rawItems: share.items ?? [],
  };
};

/**
 * Drains the native share-target slot on mount and on every warm
 * `shareReceived` event, writing the hydrated payload into
 * {@link pendingShareAtom}. Two consumers subscribe to that atom:
 *   - `ShareTargetStrip` shows the banner above the app shell while it's
 *     non-null,
 *   - `RoomInput.useEffect` drains the atom into the first chat the user
 *     opens, dropping files into the upload board and any text into the
 *     composer (see RoomInput.tsx).
 *
 * Mounted once inside the authenticated tree (see Router.tsx) — before that
 * point Matrix client / room list aren't available, so even if a cold-start
 * intent landed we couldn't route it anywhere yet. The native plugin keeps
 * the payload in its slot until JS finally drains via pickPendingShare(true),
 * so deferring is safe: cold-start path comes via BridgeActivity.load() →
 * handleOnNewIntent (Capacitor lifecycle), warm path via Activity.onNewIntent
 * → handleOnNewIntent + a `shareReceived` event.
 */
export const useShareTargetReceiver = (): void => {
  const setPendingShare = useSetAtom(pendingShareAtom);

  useEffect(() => {
    if (!isAndroidPlatform()) return undefined;

    let cancelled = false;
    let handle: PluginListenerHandle | undefined;

    // Monotonic drain id: every drain() bumps a counter, captures its own id,
    // and only commits its hydrated result if it's still the latest. Without
    // this, an in-flight hydration of a large (slow) payload can land after
    // a subsequent quick payload's hydration and overwrite the picker with
    // stale state.
    let drainSeq = 0;
    let latestDrain = 0;

    const drain = async () => {
      drainSeq += 1;
      const mySeq = drainSeq;
      latestDrain = mySeq;
      const share = await shareTarget.pickPendingShare(true);
      if (cancelled || mySeq !== latestDrain) return;
      const hydrated = await hydrateShare(share);
      if (cancelled || mySeq !== latestDrain || !hydrated) return;
      setPendingShare(hydrated);
    };

    // Register the warm listener FIRST so a `shareReceived` that lands while
    // initial drain is awaiting the slot read doesn't slip through. The
    // native plugin emits the event after stashing into the slot, so even
    // if our drain races a warm capture, the listener will fire a second
    // drain that re-reads the slot. Without this ordering, a warm share
    // arriving in the gap between `drain()` and `addListener` would leave
    // its payload stuck in the native slot with no JS-side trigger.
    shareTarget
      .addShareReceivedListener(() => {
        drain();
      })
      .then((h) => {
        if (cancelled) {
          h.remove();
        } else {
          handle = h;
        }
      })
      .catch(() => undefined);

    // Initial drain: covers cold-start (user launched Vojo straight from the
    // share-sheet) and warm app-mount after re-login.
    drain();

    return () => {
      cancelled = true;
      handle?.remove();
    };
  }, [setPendingShare]);
};
