import {
  useState,
  createContext,
  type ReactNode,
  useRef,
  useEffect,
  useContext,
  useCallback,
  useMemo,
} from "react";
import { useCrypto } from "@tensamin/crypto/context";
import { log } from "@tensamin/shared/log";
import { useStorage } from "@tensamin/storage/context";
import { createTransportClient, READY_STATE, type BoundSendFn } from "./core";
import {
  PING_INTERVAL,
  RECONNECT_RESET,
  RECONNECT_TRIES,
  RETRY_INTERVAL,
  TRANSPORT_URL,
} from "./values";
import {
  socket as schemas,
  type Socket as Schemas,
} from "@tensamin/shared/data";
import Loading from "@tensamin/ui/screens/loading";
import ErrorScreen from "@tensamin/ui/screens/error";

const FATAL_IDENTIFICATION_ERROR_TYPES = new Set([
  "error",
  "error_invalid_user_id",
  "error_no_user_id",
  "error_invalid_challenge",
  "error_invalid_secret",
  "error_invalid_private_key",
  "error_invalid_public_key",
  "error_not_authenticated",
]);

/**
 * Detects whether an error chain contains a STOP_SENDING transport signal.
 * @param error Unknown error value from transport operations.
 * @returns True when the error represents a STOP_SENDING condition.
 */
function isStopSendingError(error: unknown) {
  if (typeof error === "string") {
    return error.includes("STOP_SENDING");
  }

  if (error instanceof Error) {
    if (error.message.includes("STOP_SENDING")) {
      return true;
    }

    const errorWithCause = error as Error & { cause?: unknown };
    if (errorWithCause.cause !== undefined) {
      return isStopSendingError(errorWithCause.cause);
    }

    return false;
  }

  if (typeof error === "object" && error !== null) {
    const maybeMessage = (error as { message?: unknown }).message;
    if (typeof maybeMessage === "string") {
      return maybeMessage.includes("STOP_SENDING");
    }
  }

  return false;
}

/**
 * Classifies identification errors that should be treated as terminal.
 * @param error Unknown error raised during identification.
 * @returns True when identification should fail without retry.
 */
function isFatalIdentificationError(error: unknown) {
  if (typeof error === "object" && error !== null && "type" in error) {
    const type = (error as { type?: unknown }).type;
    if (
      typeof type === "string" &&
      FATAL_IDENTIFICATION_ERROR_TYPES.has(type)
    ) {
      return true;
    }
  }

  if (!(error instanceof Error)) {
    return false;
  }

  if (
    error.message.includes("Missing or invalid user id") ||
    error.message.includes("Missing private key") ||
    error.message.includes("Identification challenge was rejected") ||
    error.message.includes("timed out after") ||
    error.message.includes("Response validation failed")
  ) {
    return true;
  }

  return false;
}

/**
 * Extracts structured protocol error details for identification logging.
 * @param error Unknown error raised during identification.
 * @returns Structured protocol error details when available.
 */
function getProtocolErrorDetails(error: unknown) {
  if (typeof error !== "object" || error === null) {
    return null;
  }

  if (!("type" in error)) {
    return null;
  }

  const protocolError = error as {
    id?: unknown;
    type?: unknown;
    data?: unknown;
  };

  return {
    id: protocolError.id,
    type: protocolError.type,
    data: protocolError.data,
  };
}

type ContextType = {
  send: BoundSendFn<Schemas>;
  readyState: () => number;
  ownPing: () => number;
  iotaPing: () => number;
  identified: () => boolean;
};

const socketContext = createContext<ContextType | undefined>(undefined);

/**
 * Provides socket transport state and authenticated send operations to children.
 * @param props Component props with children.
 * @returns Loading, error, or provider-wrapped JSX.
 */
export default function Provider(props: {
  children: ReactNode;
  blockConnection?: boolean;
}) {
  const { load } = useStorage();
  const { decrypt, getSharedSecret } = useCrypto();

  const [readyState, setReadyState] = useState<number>(READY_STATE.CLOSED);
  const [connected, setConnected] = useState<boolean>(false);
  const [identified, setIdentified] = useState<boolean>(false);
  const [identifying, setIdentifying] = useState<boolean>(false);

  const [ownPing, setOwnPing] = useState<number>(0);
  const [iotaPing, setIotaPing] = useState<number>(0);

  const [error, setError] = useState("");
  const [errorDescription, setErrorDescription] = useState("");

  const clientRef = useRef<ReturnType<
    typeof createTransportClient<Schemas>
  > | null>(null);
  const identificationStartedRef = useRef(false);

  /**
   * Sends typed protocol messages through the active transport client.
   * @param type Protocol message type.
   * @param data Optional request payload.
   * @param options Optional request id and response mode.
   * @returns A promise for either void (no response) or typed message payload.
   */
  const send = useCallback<BoundSendFn<Schemas>>(
    ((
      type: string,
      data?: Record<string, unknown>,
      options?: { id?: number; noResponse?: boolean },
    ) => {
      const client = clientRef.current;

      if (!client) {
        return Promise.reject(new Error("Socket is not connected"));
      }

      if (options?.noResponse) {
        return client.send(type as keyof Schemas & string, data as never, {
          ...options,
          noResponse: true,
        });
      }

      return client.send(type as keyof Schemas & string, data as never, {
        ...options,
        noResponse: false,
      });
    }) as BoundSendFn<Schemas>,
    [],
  );

  useEffect(() => {
    if (!connected || !identified) {
      return;
    }

    const interval = setInterval(async () => {
      try {
        const originalNow = Date.now();

        const data = await send("ping", {
          last_ping: originalNow,
        });

        const travelTime = Date.now() - originalNow;
        setOwnPing(travelTime);

        const remotePing = data.data.ping_iota;
        if (typeof remotePing === "number") {
          setIotaPing(remotePing);
        }
      } catch (intervalError) {
        log(1, "Socket", "yellow", "Ping failed", intervalError);
      }
    }, PING_INTERVAL);

    return () => {
      clearInterval(interval);
    };
  }, [connected, identified, send]);

  useEffect(() => {
    let attempts = 0;
    let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
    let reconnectResetTimer: ReturnType<typeof setTimeout> | null = null;
    let reconnectScheduled = false;
    let disposed = false;

    /**
     * Clears any scheduled reconnect timeout and resets scheduling flags.
     * @returns Void.
     */
    const clearReconnectTimer = () => {
      if (!reconnectTimer) {
        return;
      }

      clearTimeout(reconnectTimer);
      reconnectTimer = null;
      reconnectScheduled = false;
    };

    /**
     * Clears the stability timer that resets reconnect attempt counters.
     * @returns Void.
     */
    const clearReconnectResetTimer = () => {
      if (!reconnectResetTimer) {
        return;
      }

      clearTimeout(reconnectResetTimer);
      reconnectResetTimer = null;
    };

    /**
     * Starts the stability timer that resets reconnect attempts after uptime.
     * @returns Void.
     */
    const scheduleReconnectReset = () => {
      clearReconnectResetTimer();
      reconnectResetTimer = setTimeout(() => {
        attempts = 0;
        reconnectResetTimer = null;
      }, RECONNECT_RESET * 1_000);
    };

    /**
     * Schedules a delayed reconnect attempt unless retries are exhausted.
     * @param reason Optional reason for reconnect scheduling.
     * @returns Void.
     */
    const scheduleReconnect = (reason?: unknown) => {
      if (disposed || reconnectScheduled) {
        return;
      }

      if (attempts >= RECONNECT_TRIES) {
        setError("Connection Failed");
        setErrorDescription(
          "Unable to connect to the server after multiple attempts. Please check your internet connection or try again later.",
        );
        log(0, "Socket", "red", "Reconnection attempts exhausted", reason);
        return;
      }

      attempts += 1;
      reconnectScheduled = true;
      reconnectTimer = setTimeout(() => {
        reconnectScheduled = false;
        reconnectTimer = null;
        void connect();
      }, RETRY_INTERVAL);
    };

    const transportClient = !props.blockConnection
      ? createTransportClient(schemas, {
          url: TRANSPORT_URL,
          onReadyStateChange: (state) => {
            setReadyState(state);

            if (state === READY_STATE.OPEN) {
              clearReconnectTimer();
              scheduleReconnectReset();
              identificationStartedRef.current = false;
              setConnected(true);
              setIdentified(false);
              setError("");
              setErrorDescription("");
              return;
            }

            clearReconnectResetTimer();
            identificationStartedRef.current = false;
            setConnected(false);
            setIdentified(false);
          },
          onClose: ({ error: closeError, intentional }) => {
            clearReconnectResetTimer();
            setConnected(false);
            setIdentified(false);
            setIdentifying(false);

            if (disposed || intentional) {
              return;
            }

            log(0, "Socket", "red", "Disconnected", closeError, {
              stopSending: isStopSendingError(closeError),
            });
            scheduleReconnect(closeError);
          },
        })
      : null;

    clientRef.current = transportClient;

    /**
     * Establishes the transport connection and schedules reconnect on failures.
     * @returns Promise that resolves after one connection attempt.
     */
    async function connect() {
      if (disposed) {
        return;
      }

      try {
        await transportClient?.connect(TRANSPORT_URL);
      } catch (connectError) {
        if (disposed) {
          return;
        }

        log(0, "Socket", "red", "Connection attempt failed", connectError);
        scheduleReconnect(connectError);
      }
    }

    void connect();

    return () => {
      disposed = true;
      clearReconnectTimer();
      clearReconnectResetTimer();

      if (clientRef.current === transportClient) {
        clientRef.current = null;
      }

      void transportClient?.close("context-dispose");
      setReadyState(READY_STATE.CLOSED);
      setConnected(false);
      setIdentified(false);
      setIdentifying(false);
      identificationStartedRef.current = false;
    };
  }, [props.blockConnection]);

  useEffect(() => {
    if (!connected) {
      setIdentifying(false);
      setIdentified(false);
      identificationStartedRef.current = false;
      return;
    }

    if (identificationStartedRef.current) {
      return;
    }

    identificationStartedRef.current = true;
    let cancelled = false;
    setIdentifying(true);
    setIdentified(false);

    /**
     * Executes the challenge-response identification handshake.
     * @returns Promise that resolves when identification flow completes.
     */
    const identify = async () => {
      try {
        const userId = await load("user_id");
        const privateKey = await load("private_key");

        if (!Number.isSafeInteger(userId) || userId <= 0) {
          throw new Error("Missing or invalid user id for identification");
        }

        if (privateKey.trim() === "") {
          throw new Error("Missing private key for identification");
        }

        const challengeEnvelope = await send("identification", {
          user_id: userId,
        });

        const sharedSecret = await getSharedSecret(
          privateKey,
          "",
          challengeEnvelope.data.public_key,
        );

        const decryptedChallenge = await decrypt(
          sharedSecret,
          challengeEnvelope.data.challenge,
        );

        const verification = await send("challenge_response", {
          challenge: decryptedChallenge,
        });

        if (verification.data.accepted !== true) {
          throw new Error("Identification challenge was rejected");
        }

        if (cancelled) {
          return;
        }

        setError("");
        setErrorDescription("");
        setIdentified(true);
      } catch (identificationError) {
        if (cancelled) {
          return;
        }

        if (isStopSendingError(identificationError)) {
          setError("Connection closed");
          setErrorDescription(
            "The connection was forcefully closed by the Omikron.",
          );
          setIdentified(false);
          return;
        }

        const isFatal = isFatalIdentificationError(identificationError);
        const protocolErrorDetails =
          getProtocolErrorDetails(identificationError);

        log(
          isFatal ? 0 : 1,
          "Socket",
          isFatal ? "red" : "yellow",
          "Identification handshake failed",
          protocolErrorDetails ?? identificationError,
        );

        setIdentified(false);

        setError("Identification Failed");
        setErrorDescription(
          isFatal
            ? "Unable to complete secure identification. Please verify your credentials and try again."
            : "Unable to complete secure identification because the transport request failed.",
        );
      } finally {
        if (!cancelled) {
          setIdentifying(false);
        }
      }
    };

    void identify();

    return () => {
      cancelled = true;
    };
  }, [connected, decrypt, getSharedSecret, load, send]);

  const progress = useMemo(() => {
    if (readyState === READY_STATE.CONNECTING) return 30;
    if (!connected) return 45;
    if (identifying) return 75;
    if (!identified) return 90;
    return 100;
  }, [connected, identified, identifying, readyState]);

  const loadingTitle = useMemo(() => {
    if (readyState === READY_STATE.CONNECTING || !connected) {
      return "Connecting to Tensamin";
    }

    if (identifying || !identified) {
      return "Identifying secure session";
    }

    return "Loading";
  }, [connected, identified, identifying, readyState]);

  const loadingDescription = useMemo(() => {
    if (readyState === READY_STATE.CONNECTING || !connected) {
      return "Establishing transport channel";
    }

    if (identifying || !identified) {
      return "Verifying challenge-response handshake";
    }

    return undefined;
  }, [connected, identified, identifying, readyState]);

  const contextValue = useMemo<ContextType>(
    () => ({
      send,
      readyState: () => readyState,
      ownPing: () => ownPing,
      iotaPing: () => iotaPing,
      identified: () => identified,
    }),
    [identified, iotaPing, ownPing, readyState, send],
  );

  if (error !== "" && errorDescription !== "") {
    return <ErrorScreen error={error} description={errorDescription} />;
  }

  if (!connected || !identified) {
    return (
      <Loading
        progress={progress}
        title={loadingTitle}
        description={loadingDescription}
        fullscreen
      />
    );
  }

  return (
    <socketContext.Provider value={contextValue}>
      {props.children}
    </socketContext.Provider>
  );
}

/**
 * Returns the active socket context and enforces provider usage.
 * @returns Socket context API for transport operations and connection state.
 */
export function useSocket(): ContextType {
  const context = useContext(socketContext);
  if (!context) {
    throw new Error("useSocket must be used within a SocketProvider");
  }
  return context;
}
