import type { NavigateOptions } from "../common/navigate";
import type { AutomationConfig } from "../data/automation";

const CALLBACK_EXTERNAL_BUS = "externalBus";

interface CommandInFlight {
  resolve: (data: any) => void;
  reject: (err: EMError) => void;
}

export interface EMMessage {
  id?: number;
  type: string;
}

interface EMError {
  code: string;
  message: string;
}

interface EMMessageResultSuccess {
  id: number;
  type: "result";
  success: true;
  result: unknown;
}

interface EMMessageResultError {
  id: number;
  type: "result";
  success: false;
  error: EMError;
}

interface EMOutgoingMessageConfigGet extends EMMessage {
  type: "config/get";
}

interface EMOutgoingMessageEntityAddToGetActions extends EMMessage {
  type: "entity/add_to/get_actions";
  payload: {
    entity_id: string;
  };
}

interface EMOutgoingMessageBarCodeScan extends EMMessage {
  type: "bar_code/scan";
  payload: {
    title: string;
    description: string;
    alternative_option_label?: string;
  };
}

interface EMOutgoingMessageBarCodeClose extends EMMessage {
  type: "bar_code/close";
}

interface EMOutgoingMessageBarCodeNotify extends EMMessage {
  type: "bar_code/notify";
  payload: {
    message: string;
  };
}

interface EMOutgoingMessageMatterCommission extends EMMessage {
  type: "matter/commission";
  payload?: {
    mac_extended_address: string | null;
    extended_pan_id: string | null;
    border_agent_id: string | null;
    active_operational_dataset: string | null;
  };
}

interface EMOutgoingMessageImportThreadCredentials extends EMMessage {
  type: "thread/import_credentials";
}

interface EMOutgoingMessageWithAnswer {
  "config/get": {
    request: EMOutgoingMessageConfigGet;
    response: ExternalConfig;
  };
  "entity/add_to/get_actions": {
    request: EMOutgoingMessageEntityAddToGetActions;
    response: ExternalEntityAddToActions;
  };
}

interface EMOutgoingMessageExoplayerPlayHLS extends EMMessage {
  type: "exoplayer/play_hls";
  payload: {
    url: string;
    muted: boolean;
  };
}
interface EMOutgoingMessageExoplayerResize extends EMMessage {
  type: "exoplayer/resize";
  payload: {
    left: number;
    top: number;
    right: number;
    bottom: number;
  };
}

interface EMOutgoingMessageExoplayerStop extends EMMessage {
  type: "exoplayer/stop";
}

interface EMOutgoingMessageThemeUpdate extends EMMessage {
  type: "theme-update";
}

interface EMOutgoingMessageHaptic extends EMMessage {
  type: "haptic";
  payload: { hapticType: string };
}

interface EMOutgoingMessageConnectionStatus extends EMMessage {
  type: "connection-status";
  payload: { event: string };
}

interface EMOutgoingMessageAppConfiguration extends EMMessage {
  type: "config_screen/show";
}

interface EMOutgoingMessageTagWrite extends EMMessage {
  type: "tag/write";
  payload: {
    name: string | null;
    tag: string;
  };
}

interface EMOutgoingMessageSidebarShow extends EMMessage {
  type: "sidebar/show";
}

interface EMOutgoingMessageAssistShow extends EMMessage {
  type: "assist/show";
  payload?: {
    pipeline_id: "preferred" | "last_used" | string;
    start_listening: boolean;
  };
}

interface EMOutgoingMessageAssistSettings extends EMMessage {
  type: "assist/settings";
}

interface EMOutgoingMessageImprovScan extends EMMessage {
  type: "improv/scan";
}

interface EMOutgoingMessageImprovConfigureDevice extends EMMessage {
  type: "improv/configure_device";
  payload: {
    name: string;
  };
}

interface EMOutgoingMessageThreadStoreInPlatformKeychain extends EMMessage {
  type: "thread/store_in_platform_keychain";
  payload: {
    mac_extended_address: string | null;
    border_agent_id: string | null;
    active_operational_dataset: string;
    extended_pan_id: string;
  };
}

interface EMOutgoingMessageAddEntityTo extends EMMessage {
  type: "entity/add_to";
  payload: {
    entity_id: string;
    app_payload: string; // Opaque string received from get_actions
  };
}

interface EMOutgoingMessageFocusElement extends EMMessage {
  type: "focus_element";
  payload: {
    element_id: string;
  };
}

// These types are handled internally by the Android app via postMessage.
// They are not sent by the frontend and should not be used directly.
// They are intentionally listed here to prevent anyone from using them unintentionally.
type RejectedEMMessageType = "onHomeAssistantSetTheme" | "handleBlob";

type EMOutgoingMessageWithoutAnswer =
  | EMMessageResultError
  | EMMessageResultSuccess
  | EMOutgoingMessageAppConfiguration
  | EMOutgoingMessageAssistShow
  | EMOutgoingMessageBarCodeClose
  | EMOutgoingMessageBarCodeNotify
  | EMOutgoingMessageBarCodeScan
  | EMOutgoingMessageConnectionStatus
  | EMOutgoingMessageExoplayerPlayHLS
  | EMOutgoingMessageExoplayerResize
  | EMOutgoingMessageExoplayerStop
  | EMOutgoingMessageHaptic
  | EMOutgoingMessageImportThreadCredentials
  | EMOutgoingMessageMatterCommission
  | EMOutgoingMessageSidebarShow
  | EMOutgoingMessageTagWrite
  | EMOutgoingMessageThemeUpdate
  | EMOutgoingMessageThreadStoreInPlatformKeychain
  | EMOutgoingMessageImprovScan
  | EMOutgoingMessageImprovConfigureDevice
  | EMOutgoingMessageAddEntityTo
  | EMOutgoingMessageFocusElement
  | EMOutgoingMessageAssistSettings;

export interface EMIncomingMessageRestart {
  id: number;
  type: "command";
  command: "restart";
}
export interface EMIncomingMessageNavigate {
  id: number;
  type: "command";
  command: "navigate";
  payload: {
    path: string;
    options?: NavigateOptions;
  };
}

export interface EMIncomingMessageShowNotifications {
  id: number;
  type: "command";
  command: "notifications/show";
}

export interface EMIncomingMessageToggleSidebar {
  id: number;
  type: "command";
  command: "sidebar/toggle";
}

export interface EMIncomingMessageShowSidebar {
  id: number;
  type: "command";
  command: "sidebar/show";
}

export interface EMIncomingMessageShowAutomationEditor {
  id: number;
  type: "command";
  command: "automation/editor/show";
  payload?: {
    config?: Partial<AutomationConfig>;
  };
}

export interface EMIncomingMessageBarCodeScanResult {
  id: number;
  type: "command";
  command: "bar_code/scan_result";
  payload: {
    // A string decoded from the barcode data.
    rawValue: string;
    // https://developer.mozilla.org/en-US/docs/Web/API/Barcode_Detection_API#supported_barcode_formats
    format:
      | "aztec"
      | "code_128"
      | "code_39"
      | "code_93"
      | "codabar"
      | "data_matrix"
      | "ean_13"
      | "ean_8"
      | "itf"
      | "pdf417"
      | "qr_code"
      | "upc_a"
      | "upc_e"
      | "unknown";
  };
}

export interface EMIncomingMessageBarCodeScanAborted {
  id: number;
  type: "command";
  command: "bar_code/aborted";
  payload: {
    reason: "canceled" | "alternative_options";
  };
}

export interface ImprovDiscoveredDevice {
  name: string;
}

export interface EMIncomingMessageImprovDeviceDiscovered extends EMMessage {
  id: number;
  type: "command";
  command: "improv/discovered_device";
  payload: ImprovDiscoveredDevice;
}

export interface EMIncomingMessageImprovDeviceSetupDone extends EMMessage {
  id: number;
  type: "command";
  command: "improv/device_setup_done";
}

export interface EMIncomingMessageKioskModeSet {
  id: number;
  type: "command";
  command: "kiosk_mode/set";
  payload: {
    enable: boolean;
  };
}

export interface MatterCommissionFinish {
  name: string | null;
  success: boolean;
}

export interface EMIncomingMessageMatterCommissionFinish extends EMMessage {
  id: number;
  type: "command";
  command: "matter/commission/finish";
  payload: MatterCommissionFinish;
}

export type EMIncomingMessageCommands =
  | EMIncomingMessageRestart
  | EMIncomingMessageNavigate
  | EMIncomingMessageShowNotifications
  | EMIncomingMessageToggleSidebar
  | EMIncomingMessageShowSidebar
  | EMIncomingMessageShowAutomationEditor
  | EMIncomingMessageBarCodeScanResult
  | EMIncomingMessageBarCodeScanAborted
  | EMIncomingMessageImprovDeviceDiscovered
  | EMIncomingMessageImprovDeviceSetupDone
  | EMIncomingMessageMatterCommissionFinish
  | EMIncomingMessageKioskModeSet;

type EMIncomingMessage =
  | EMMessageResultSuccess
  | EMMessageResultError
  | EMIncomingMessageCommands;

type EMIncomingMessageHandler = (msg: EMIncomingMessageCommands) => boolean;

export interface ExternalConfig {
  hasSettingsScreen?: boolean;
  hasSidebar?: boolean;
  canWriteTag?: boolean;
  hasExoPlayer?: boolean;
  canCommissionMatter?: boolean;
  hasMatterStatusReport?: boolean;
  canImportThreadCredentials?: boolean;
  canTransferThreadCredentialsToKeychain?: boolean;
  hasAssist?: boolean;
  hasBarCodeScanner?: number;
  canSetupImprov?: boolean;
  appVersion?: string;
  hasEntityAddTo?: boolean; // Supports "Add to" from more-info dialog, with action coming from external app
  hasAssistSettings?: boolean; // Shows the "This device" section in voice assistant settings
}

export interface ExternalEntityAddToAction {
  enabled: boolean;
  name: string; // Translated name of the action to be displayed in the UI
  details?: string; // Optional translated details of the action to be displayed in the UI
  mdi_icon: string; // MDI icon name to be displayed in the UI (e.g., "mdi:car")
  app_payload: string; // Opaque string to be sent back when the action is selected
}

export interface ExternalEntityAddToActions {
  actions: ExternalEntityAddToAction[];
}

export class ExternalMessaging {
  public config!: ExternalConfig;

  public commands: Record<number, CommandInFlight> = {};

  public msgId = 0;

  private _commandHandler?: EMIncomingMessageHandler;

  public async attach() {
    window[CALLBACK_EXTERNAL_BUS] = (msg) => this.receiveMessage(msg);
    window.addEventListener("connection-status", (ev) =>
      this.fireMessage({
        type: "connection-status",
        payload: { event: ev.detail },
      })
    );
    this.config = await this.sendMessage<"config/get">({
      type: "config/get",
    });
  }

  public addCommandHandler(handler: EMIncomingMessageHandler) {
    this._commandHandler = handler;
  }

  /**
   * Send message to external app that expects a response.
   * @param msg message to send
   */
  public sendMessage<
    T extends keyof EMOutgoingMessageWithAnswer,
    TType extends string = EMOutgoingMessageWithAnswer[T]["request"]["type"],
  >(
    msg: EMOutgoingMessageWithAnswer[T]["request"] & {
      type: TType &
        (TType extends RejectedEMMessageType
          ? "ERROR: message type is rejected"
          : {});
    }
  ): Promise<EMOutgoingMessageWithAnswer[T]["response"]> {
    const msgId = ++this.msgId;
    msg.id = msgId;

    this._sendExternal(msg);

    return new Promise<EMOutgoingMessageWithAnswer[T]["response"]>(
      (resolve, reject) => {
        this.commands[msgId] = { resolve, reject };
      }
    );
  }

  /**
   * Send message to external app without expecting a response.
   * @param msg message to send
   */
  public fireMessage<T extends string>(
    msg: EMOutgoingMessageWithoutAnswer & {
      type: T &
        (T extends RejectedEMMessageType
          ? "ERROR: message type is rejected"
          : {});
    }
  ) {
    if (!msg.id) {
      msg.id = ++this.msgId;
    }
    this._sendExternal(msg);
  }

  public receiveMessage(msg: EMIncomingMessage) {
    if (__DEV__) {
      // eslint-disable-next-line no-console
      console.log("Receiving message from external app", msg);
    }

    if (msg.type === "command") {
      if (!this._commandHandler || !this._commandHandler(msg)) {
        let code: string;
        let message: string;
        if (this._commandHandler) {
          code = "not_ready";
          message = "Command handler not ready";
        } else {
          code = "unknown_command";
          message = `Unknown command ${msg.command}`;
        }
        // eslint-disable-next-line no-console
        console.warn(message, msg);
        this.fireMessage({
          id: msg.id,
          type: "result",
          success: false,
          error: {
            code,
            message,
          },
        });
      }
      return;
    }

    const pendingCmd = this.commands[msg.id];

    if (!pendingCmd) {
      // eslint-disable-next-line no-console
      console.warn(`Received unknown msg ID`, msg.id);
      return;
    }

    if (msg.type === "result") {
      if (msg.success) {
        pendingCmd.resolve(msg.result);
      } else {
        pendingCmd.reject(msg.error);
      }
    }
  }

  protected _sendExternal(msg: EMMessage) {
    if (__DEV__) {
      // eslint-disable-next-line no-console
      console.log("Sending message to external app", msg);
    }
    if (window.externalAppV2) {
      window.externalAppV2.postMessage(
        JSON.stringify({ type: "externalBus", payload: msg })
      );
    } else if (window.externalApp) {
      window.externalApp.externalBus(JSON.stringify(msg));
    } else {
      window.webkit!.messageHandlers.externalBus.postMessage(msg);
    }
  }
}
