import { useCallback, useEffect, useRef, useState } from "react";
import { useAtomSet } from "@effect/atom-react";
import * as Data from "effect/Data";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";

import { cancelOAuth, oauthConnectionCompleted, startOAuth } from "../api/atoms";
import { trackEvent } from "../api/analytics";
import { messageFromExit, messageFromUnknown, useReportHandledError } from "../api/error-reporting";
import {
  openOAuthPopup,
  openOAuthSystemBrowser,
  reserveOAuthPopup,
  type OAuthPopupResult,
  type ReservedOAuthPopup,
} from "../api/oauth-popup";
import { connectionWriteKeys } from "../api/reactivity-keys";
import { getActiveOrgSlug } from "../api/server-connection";

export type DesktopBridge = {
  readonly openExternal: (url: string) => Promise<void>;
};

const getDesktopBridge = (): DesktopBridge | null => {
  if (typeof window === "undefined") return null;
  const candidate = (window as { readonly executor?: Partial<DesktopBridge> }).executor;
  return candidate && typeof candidate.openExternal === "function"
    ? // oxlint-disable-next-line executor/no-double-cast -- boundary: narrowed by the typeof guard above
      (candidate as DesktopBridge)
    : null;
};
import { Button } from "../components/button";
import {
  OAUTH_POPUP_MESSAGE_TYPE,
  OAuthState,
  type AuthTemplateSlug,
  type ConnectionName,
  type IntegrationSlug,
  type OAuthClientSlug,
  type Owner,
} from "@executor-js/sdk/shared";

// ---------------------------------------------------------------------------
// OAuth sign-in (v2). A registered OAuth client (`oauth.createClient`) is run by
// `oauth.start` for one integration, minting an owner-scoped Connection. `start`
// either returns the connection inline (`status: "connected"`) or a redirect
// (`status: "redirect"` with an `authorizationUrl` + `state`); the popup
// completes the redirect via the server `/oauth/callback`, then posts the result
// back. `state` is the correlation token (the v1 session id).
//
// NOTE(v2): the server-side flow is stubbed (D18). The shapes below track the
// v2 contract so this UI compiles; the plugin `/react` wave wires the buttons.
// ---------------------------------------------------------------------------

export type OAuthCompletionPayload = {
  readonly owner: Owner;
  readonly integration: IntegrationSlug;
  readonly name: ConnectionName;
  readonly identityLabel?: string | null;
};

export type OAuthStartPayload = {
  /** Registered OAuth client slug to run. */
  readonly client: OAuthClientSlug;
  /** Owner of `client` (a Personal connection may use a shared Workspace app). */
  readonly clientOwner: Owner;
  readonly owner: Owner;
  /** Name for the connection the flow mints. */
  readonly name: ConnectionName;
  readonly integration: IntegrationSlug;
  readonly template: AuthTemplateSlug;
  readonly identityLabel?: string;
  /** Mint a NEW connection: the server resolves a taken `name` to the next
   *  free suffixed one. Omitted on reconnect, which targets the existing row. */
  readonly newConnection?: boolean;
  readonly redirectUri?: string;
};

export type StartOAuthPopupInput<TPayload extends OAuthCompletionPayload> = {
  readonly payload: OAuthStartPayload;
  readonly onSuccess: (payload: TPayload) => void | Promise<void>;
  readonly onError?: (error: string) => void;
  readonly onAuthorizationStarted?: (result: OAuthAuthorizationStartResult) => void;
  /** Window reserved on the click that began this flow. See `reserve`. */
  readonly reservation?: OAuthPopupReservation;
};

/**
 * A sign-in window claimed synchronously inside a click handler.
 *
 * `window.open` requires transient user activation, which Chromium expires
 * about five seconds after the click. Any flow that awaits the network before
 * it has an authorization URL (DCR probes then registers, CIMD creates a
 * client, reconnect starts a session) therefore cannot open the window when the
 * URL finally arrives: by then the activation is gone and the browser refuses
 * silently. Those flows reserve a blank window on the click and navigate it
 * later, however long the round trips take.
 *
 * Desktop hosts hand the URL to the real browser, so they reserve nothing and
 * carry the bridge that will open it instead.
 */
export type OAuthPopupReservation =
  | { readonly kind: "desktop"; readonly bridge: DesktopBridge }
  | { readonly kind: "window"; readonly popup: ReservedOAuthPopup }
  | { readonly kind: "blocked" };

const POPUP_BLOCKED_MESSAGE = "Sign-in popup was blocked by the browser";

export type OAuthAuthorizationStartResult = {
  /** OAuth correlation token (was the v1 session id). */
  readonly state: string;
  readonly authorizationUrl: string | null;
};

class OAuthAuthorizationStartError extends Data.TaggedError("OAuthAuthorizationStartError")<{
  readonly cause: unknown;
  readonly message: string;
}> {}

export type StartOAuthAuthorizationInput<TPayload extends OAuthCompletionPayload> = {
  readonly owner: Owner;
  readonly run: () => Promise<OAuthAuthorizationStartResult>;
  readonly onSuccess: (payload: TPayload) => void | Promise<void>;
  readonly onError?: (error: string, details?: string) => void;
  readonly onAuthorizationStarted?: (result: OAuthAuthorizationStartResult) => void;
  readonly reportMetadata?: Record<string, string | number | boolean | null | undefined>;
  /** Window reserved on the click that began this flow. See `reserve`. */
  readonly reservation?: OAuthPopupReservation;
};

export function oauthCallbackUrl(path = "/api/oauth/callback"): string {
  if (typeof window === "undefined") return path;
  return new URL(path, window.location.origin).toString();
}

export const OAUTH_CLIENT_ID_METADATA_DOCUMENT_PATH = "/api/oauth/client-id-metadata/default.json";
export const OAUTH_CLIENT_ID_METADATA_DOCUMENT_LOCAL_PATH =
  "/api/oauth/client-id-metadata/local.json";

const configuredClientIdMetadataBaseUrl = (): string | undefined => {
  const env = (
    import.meta as ImportMeta & {
      readonly env?: Record<string, string | undefined>;
    }
  ).env;
  const value = env?.VITE_EXECUTOR_CIMD_CLIENT_ID_METADATA_BASE_URL?.trim();
  return value ? value : undefined;
};

export function oauthClientIdMetadataDocumentUrl(options?: {
  readonly hostedBaseUrl?: string | null;
  readonly path?: string;
}): string {
  const hostedBaseUrl = options?.hostedBaseUrl ?? configuredClientIdMetadataBaseUrl();
  if (hostedBaseUrl) {
    return new URL(OAUTH_CLIENT_ID_METADATA_DOCUMENT_LOCAL_PATH, hostedBaseUrl).toString();
  }

  const defaultPath = options?.path ?? OAUTH_CLIENT_ID_METADATA_DOCUMENT_PATH;
  if (typeof window === "undefined") return defaultPath;
  const orgSlug = getActiveOrgSlug();
  const metadataPath = orgSlug
    ? `/api/oauth/client-id-metadata/${encodeURIComponent(orgSlug)}.json`
    : defaultPath;
  const url = new URL(metadataPath, window.location.origin);
  return url.toString();
}

export function useOAuthPopupFlow<
  TPayload extends OAuthCompletionPayload = OAuthCompletionPayload,
>(options: {
  readonly popupName: string;
  readonly callbackPath?: string;
  readonly noAuthorizationUrlMessage?: string;
  readonly popupBlockedMessage?: string;
  readonly popupClosedMessage?: string;
  readonly detectPopupClosed?: boolean;
  readonly startErrorMessage?: string;
}) {
  const {
    detectPopupClosed = true,
    callbackPath,
    noAuthorizationUrlMessage,
    popupBlockedMessage,
    popupClosedMessage,
    popupName,
    startErrorMessage,
  } = options;
  const doStartOAuth = useAtomSet(startOAuth, { mode: "promiseExit" });
  const doCancelOAuth = useAtomSet(cancelOAuth, { mode: "promiseExit" });
  const doOAuthConnectionCompleted = useAtomSet(oauthConnectionCompleted, { mode: "promiseExit" });
  const reportHandledError = useReportHandledError();
  const blockedMessage = popupBlockedMessage ?? POPUP_BLOCKED_MESSAGE;
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const cleanupRef = useRef<(() => void) | null>(null);
  const sessionRef = useRef<{ readonly state: string } | null>(null);
  // A window reserved on the click but not yet handed to a flow owns nothing
  // that would close it: `cleanupRef` is only set once the authorization URL
  // exists. Hold it here so cancel and unmount can close it, or abandoning a
  // connect mid-probe strands a blank popup on screen.
  const reservationRef = useRef<ReservedOAuthPopup | null>(null);

  const releaseReservation = useCallback(() => {
    reservationRef.current?.popup.close();
    reservationRef.current = null;
  }, []);

  const cancelSession = useCallback(
    (state: string) => {
      void doCancelOAuth({ payload: { state: OAuthState.make(state) } });
    },
    [doCancelOAuth],
  );

  const cancel = useCallback(() => {
    const session = sessionRef.current;
    cleanupRef.current?.();
    cleanupRef.current = null;
    sessionRef.current = null;
    releaseReservation();
    if (session) cancelSession(session.state);
    setBusy(false);
  }, [cancelSession, releaseReservation]);

  useEffect(
    () => () => {
      const session = sessionRef.current;
      cleanupRef.current?.();
      cleanupRef.current = null;
      sessionRef.current = null;
      reservationRef.current?.popup.close();
      reservationRef.current = null;
      if (session) cancelSession(session.state);
    },
    [cancelSession],
  );

  /**
   * Claim the sign-in window NOW, while the click still carries user
   * activation, and return what was claimed so the caller can abandon a flow
   * the browser already refused instead of running a doomed authorization.
   *
   * Callers that reach `openAuthorization` synchronously do not need this; it
   * reserves for itself.
   */
  const reserve = useCallback((): OAuthPopupReservation => {
    // Tear the previous flow down BEFORE opening: `window.open` reuses a window
    // already carrying `popupName`, so cancelling afterwards would close the
    // very window just reserved.
    cancel();
    const bridge = getDesktopBridge();
    if (bridge !== null) return { kind: "desktop", bridge };
    const popup = reserveOAuthPopup({ popupName });
    if (popup === null) {
      trackEvent("oauth_popup_blocked");
      setError(blockedMessage);
      return { kind: "blocked" };
    }
    reservationRef.current = popup;
    return { kind: "window", popup };
  }, [blockedMessage, cancel, popupName]);

  const openAuthorization = useCallback(
    async (input: StartOAuthAuthorizationInput<TPayload>) => {
      // A caller that reserved on its own click already ran `cancel` inside
      // `reserve`; cancelling again here would close the window it reserved.
      const reservation = input.reservation ?? reserve();
      if (reservation.kind === "blocked") {
        setBusy(false);
        setError(blockedMessage);
        input.onError?.(blockedMessage);
        return;
      }
      setBusy(true);
      setError(null);
      // Desktop hosts open the auth URL in the user's real browser, so they
      // reserve no in-page window and rely on the polling channel for the
      // result.
      const desktopBridge = reservation.kind === "desktop" ? reservation.bridge : null;
      const reservedPopup = reservation.kind === "window" ? reservation.popup : null;
      // The window's lifetime now belongs to this flow's teardown, which closes
      // it on every failure path below.
      reservationRef.current = null;
      const startExit = await Effect.runPromiseExit(
        Effect.tryPromise({
          try: input.run,
          catch: (cause) =>
            new OAuthAuthorizationStartError({
              cause,
              message: messageFromUnknown(cause, startErrorMessage ?? "Failed to start sign-in"),
            }),
        }),
      );
      if (Exit.isFailure(startExit)) {
        const message = messageFromExit(startExit, startErrorMessage ?? "Failed to start sign-in");
        reportHandledError(startExit.cause, {
          surface: "oauth",
          action: "start",
          message,
          metadata: input.reportMetadata,
        });
        reservedPopup?.popup.close();
        setBusy(false);
        setError(message);
        input.onError?.(message);
        return;
      }
      const response = startExit.value;
      if (response.authorizationUrl === null) {
        const message =
          noAuthorizationUrlMessage ?? "OAuth start did not produce an authorization URL";
        reservedPopup?.popup.close();
        setBusy(false);
        setError(message);
        input.onError?.(message);
        return;
      }

      sessionRef.current = { state: response.state };
      input.onAuthorizationStarted?.(response);
      const handleResult = async (result: OAuthPopupResult<TPayload>) => {
        cleanupRef.current = null;
        sessionRef.current = null;

        if (!result.ok) {
          trackEvent("oauth_completed", { success: false });
          setBusy(false);
          setError(result.error);
          input.onError?.(result.error, result.errorDetails);
          return;
        }

        const refreshExit = await doOAuthConnectionCompleted({
          reactivityKeys: connectionWriteKeys,
        });
        if (Exit.isFailure(refreshExit)) {
          const message = messageFromExit(refreshExit, "Failed to refresh connection");
          reportHandledError(refreshExit.cause, {
            surface: "oauth",
            action: "refresh_connection",
            message,
            metadata: input.reportMetadata,
          });
          trackEvent("oauth_completed", { success: false });
          setBusy(false);
          setError(message);
          input.onError?.(message);
          return;
        }

        const persistenceError = await Promise.resolve(input.onSuccess(result)).then(
          () => null,
          (cause: unknown) => cause,
        );
        if (persistenceError !== null) {
          const message = messageFromUnknown(persistenceError, "Failed to save connection");
          reportHandledError(persistenceError, {
            surface: "oauth",
            action: "persist_connection",
            message,
            metadata: input.reportMetadata,
          });
          trackEvent("oauth_completed", { success: false });
          setBusy(false);
          setError(message);
          input.onError?.(message);
          return;
        }
        trackEvent("oauth_completed", { success: true });
        setBusy(false);
      };
      const handleClosed = () => {
        cleanupRef.current = null;
        sessionRef.current = null;
        // `popup.closed` is advisory: COOP redirects can make a live popup
        // appear closed to the opener. Keep server OAuth state alive for a
        // callback or TTL cleanup; only explicit cancel deletes the session.
        const message =
          popupClosedMessage ?? "Sign-in cancelled - popup was closed before completing the flow.";
        trackEvent("oauth_completed", { success: false });
        setBusy(false);
        setError(message);
        input.onError?.(message);
      };
      const handleOpenFailed = () => {
        cleanupRef.current = null;
        sessionRef.current = null;
        cancelSession(response.state);
        trackEvent("oauth_completed", { success: false });
        setBusy(false);
        setError(blockedMessage);
        input.onError?.(blockedMessage);
      };

      cleanupRef.current = desktopBridge
        ? openOAuthSystemBrowser<TPayload>({
            url: response.authorizationUrl,
            sessionId: response.state,
            openExternal: desktopBridge.openExternal,
            onResult: (result) => void handleResult(result),
            onOpenFailed: handleOpenFailed,
            onTimeout: handleClosed,
          })
        : openOAuthPopup<TPayload>({
            url: response.authorizationUrl,
            popupName,
            channelName: OAUTH_POPUP_MESSAGE_TYPE,
            expectedSessionId: response.state,
            reservedPopup: reservedPopup ?? undefined,
            closedPollMs: detectPopupClosed ? undefined : null,
            onResult: (result) => void handleResult(result),
            onClosed: handleClosed,
            onOpenFailed: handleOpenFailed,
          });
    },
    [
      blockedMessage,
      cancelSession,
      detectPopupClosed,
      doOAuthConnectionCompleted,
      noAuthorizationUrlMessage,
      popupClosedMessage,
      popupName,
      reportHandledError,
      reserve,
      startErrorMessage,
    ],
  );

  const start = useCallback(
    async (input: StartOAuthPopupInput<TPayload>) => {
      await openAuthorization({
        owner: input.payload.owner,
        onSuccess: input.onSuccess,
        onError: input.onError,
        onAuthorizationStarted: input.onAuthorizationStarted,
        ...(input.reservation === undefined ? {} : { reservation: input.reservation }),
        reportMetadata: {
          client: String(input.payload.client),
          integration: String(input.payload.integration),
          name: String(input.payload.name),
          owner: input.payload.owner,
        },
        run: () =>
          doStartOAuth({
            payload: {
              client: input.payload.client,
              clientOwner: input.payload.clientOwner,
              owner: input.payload.owner,
              name: input.payload.name,
              integration: input.payload.integration,
              template: input.payload.template,
              identityLabel: input.payload.identityLabel,
              newConnection: input.payload.newConnection,
              redirectUri: input.payload.redirectUri ?? oauthCallbackUrl(callbackPath),
            },
          }).then((exit) =>
            Exit.isSuccess(exit)
              ? // The redirect branch carries `authorizationUrl` + `state`; the
                // inline "connected" (client_credentials) branch has no URL to
                // open and no redirect, so `state` is intentionally empty — it
                // is never read for an already-minted connection.
                exit.value.status === "redirect"
                ? { state: exit.value.state, authorizationUrl: exit.value.authorizationUrl }
                : { state: "", authorizationUrl: null }
              : Effect.runPromise(
                  Effect.fail({
                    message: messageFromExit(exit, startErrorMessage ?? "Failed to start sign-in"),
                  }),
                ),
          ),
      });
    },
    [callbackPath, doStartOAuth, openAuthorization, startErrorMessage],
  );

  return {
    busy,
    error,
    setError,
    start,
    openAuthorization,
    cancel,
    reserve,
    releaseReservation,
  };
}

export function OAuthSignInButton(props: {
  readonly busy: boolean;
  readonly error: string | null;
  readonly isConnected: boolean;
  readonly onSignIn: () => void;
  readonly reconnectingLabel?: string;
  readonly signingInLabel?: string;
  readonly reconnectLabel?: string;
  readonly signInLabel?: string;
}) {
  return (
    <div className="flex items-center gap-2">
      {props.error && <span className="text-xs text-destructive">{props.error}</span>}
      <Button variant="outline" size="sm" onClick={props.onSignIn} disabled={props.busy}>
        {props.busy
          ? props.isConnected
            ? (props.reconnectingLabel ?? "Reconnecting...")
            : (props.signingInLabel ?? "Signing in...")
          : props.isConnected
            ? (props.reconnectLabel ?? "Reconnect")
            : (props.signInLabel ?? "Sign in")}
      </Button>
    </div>
  );
}
