import type { ViewerSettings } from "@mission-core/plugin-sdk";
import { xgridsK1Actions, xgridsK1Manifest } from "./manifest";

const PLUGIN_ID = xgridsK1Manifest.metadata.id;

export interface BleDevice {
  device_id: string;
  name?: string | null;
  rssi?: number | null;
  address?: string | null;
  connectable?: boolean | null;
  likely_k1?: boolean | null;
}

export type SourceMode = "idle" | "live" | "replay";

export type AcquisitionState =
  | "preparing"
  | "prepared"
  | "awaiting_external_start"
  | "starting"
  | "acquiring"
  | "awaiting_external_stop"
  | "stopping"
  | "finalizing"
  | "completed"
  | "failed"
  | "aborted"
  | "interrupted";

export type OperationStatus =
  | "accepted"
  | "running"
  | "operator_action_required"
  | "succeeded"
  | "failed"
  | "cancelled"
  | "timed_out"
  | "interrupted";

export interface XgridsDeviceRef {
  device_id: string;
  model_id: string;
  identity_stability: "stable" | "provisional";
  identity_basis:
    | "hardware-identifier"
    | "plugin-derived"
    | "operator-assigned"
    | "transport-local";
  transport_alias?: string | null;
}

export interface XgridsDeviceSession {
  device_session_id: string;
  device_id: string;
  opened_at?: string | null;
  compatibility_profile_id?: string | null;
  connectivity?: "unknown" | "offline" | "connecting" | "connected" | "degraded";
}

export interface XgridsCompatibilityState {
  profile_id?: string | null;
  decision?: "compatible" | "limited" | "unknown" | "incompatible";
  permitted_mode?: "blocked" | "evidence-only" | "read-only" | "active-control";
  firmware_claim?: string | null;
  vendor_writes_enabled?: boolean;
  camera_preview?: string | null;
  attestation?: CompatibilityAttestation | null;
}

export interface XgridsModelingControlSafety {
  mode: "shadow-only";
  status_reports_observed: number;
  decode_errors: number;
  vendor_identity_observed: boolean;
  device_serial_observed: boolean;
  identity_conflict: boolean;
  session_state?: string | null;
  project_bound: boolean;
  ready_for_start_shadow: boolean;
  ready_for_stop_shadow: boolean;
  vendor_writes_enabled: false;
  publisher_installed: false;
}

export interface XgridsApplicationControlExecution {
  mode: "dormant-write-disabled";
  state: "disarmed" | "armed-shadow-only" | "expired" | "closed";
  lease?: {
    state: "armed" | "expired" | "closed";
    remaining_seconds: number;
    authority_cached: boolean;
    exportable: false;
  } | null;
  orchestrator?: Record<string, unknown> | null;
  publisher: {
    mode: "write-disabled";
    denied_attempts: number;
    transport_calls: 0;
    vendor_writes_enabled: false;
    publisher_armed: false;
    automatic_retry: false;
  };
  live_transport_installed: false;
  can_emit_requests: false;
}

export type XgridsApplicationControlPhase =
  | "idle"
  | "connecting"
  | "connection-ready"
  | "workspace-requested"
  | "workspace-ready"
  | "project-requested"
  | "project-ready"
  | "start-requested"
  | "initializing"
  | "scanning"
  | "stop-requested"
  | "stopping"
  | "awaiting-standby-confirmation"
  | "completed"
  | "closed"
  | "failed";

export interface XgridsApplicationControlSession {
  mode: "interactive-canonical";
  state: XgridsApplicationControlPhase;
  control_socket_open: boolean;
  can_open: boolean;
  can_enter_workspace: boolean;
  can_prepare_project: boolean;
  can_start: boolean;
  can_stop: boolean;
  can_confirm_standby: boolean;
  pending_operator_action?: string | null;
  scripted_transitions: false;
  automatic_retry: false;
  outcome_unknown: boolean;
  failure?: {
    code?: string;
    reason_code?: string;
    message?: string;
    failed_phase?: string | null;
    dialogue_stage?: string | null;
    transport_state?: string | null;
    publish_attempts?: number | null;
    qos2_completions?: number | null;
    correlated_responses?: number | null;
    ignored_known_responses?: number | null;
    late_known_responses?: number | null;
    modeling_command_attempted?: boolean | null;
    diagnostic_snapshot_unavailable?: string[];
    diagnostic_evidence_unavailable?: string[];
    correlation_failure?: {
      phase?: string;
      operation_key?: string;
      response_topic?: string;
      reason_code?: string;
      reason?: string;
    } | null;
    compatibility_failure?: {
      phase?: string;
      operation_key?: string;
      response_topic?: string;
      reason_code?: string;
      expected?: Record<string, unknown>;
      observed?: Record<string, unknown>;
    } | null;
    safe_to_retry?: boolean;
  } | null;
  dialogue?: Record<string, unknown> | null;
  transport?: Record<string, unknown> | null;
}

export interface XgridsAcquisition {
  schema_version?: string;
  acquisition_id: string;
  device_id: string;
  device_session_id: string;
  compatibility_profile_id: string;
  control_mode: "operator-manual" | "plugin-commanded" | "observe-only";
  project_name?: string | null;
  mount_type?: "handheld" | null;
  gnss_mode?: "none" | null;
  cleanup_pending?: boolean;
  requested_streams: string[];
  target_host: string;
  duration_seconds: number | null;
  evidence_policy: "required" | "best-effort" | "disabled";
  state: AcquisitionState;
  state_revision: number;
  created_at?: string | null;
  updated_at?: string | null;
  message_code?: string | null;
  operator_instructions?: string[];
  result?: Record<string, unknown> | null;
}

export interface XgridsOperation {
  schema_version?: string;
  operation_id: string;
  action: string;
  status: OperationStatus;
  accepted_at?: string | null;
  completed_at?: string | null;
  deadline_at?: string | null;
  device_id?: string | null;
  device_session_id?: string | null;
  idempotency_key?: string | null;
  stage_code?: string | null;
  message_code?: string | null;
  sequence?: number;
  state_revision?: number;
  cancellable?: boolean;
  cancel_requested?: boolean;
  result?: Record<string, unknown> | null;
  error?: Record<string, unknown> | null;
  evidence_refs?: string[];
}

export interface XgridsK1Metrics {
  mqtt_to_decode_ms?: number | null;
  decode_ms?: number | null;
  publish_ms?: number | null;
  pipeline_ms?: number | null;
  end_to_end_ms?: number | null;
  frame_rate?: number | null;
  frame_rate_hz?: number | null;
  point_count?: number | null;
  dropped_preview_frames?: number | null;
  ai_end_to_end_ms?: number | null;
  ai_end_to_end_p95_ms?: number | null;
  ai_frame_rate_hz?: number | null;
  ai_dropped_frames?: number | null;
  ai_stale_ms?: number | null;
  device_elapsed_seconds?: number | null;
  device_route_distance_meters?: number | null;
  device_speed_meters_per_second?: number | null;
  device_speed_mps?: number | null;
  elapsed_seconds?: number | null;
  route_distance_meters?: number | null;
  speed_meters_per_second?: number | null;
  [key: string]: number | null | undefined;
}

export interface XgridsCameraPreviewDelivery {
  id: string;
  kind: "mse-fmp4-websocket";
  url: string;
  media_type: string;
}

export interface XgridsCameraPreviewActivation {
  group_id: string;
  max_active: number;
  selected: boolean;
  controllable: boolean;
}

export interface XgridsCameraPreviewState {
  phase?: string | null;
  revision?: number | null;
  generation?: number | null;
  active_source_id?: string | null;
  delivery?: XgridsCameraPreviewDelivery | null;
}

export interface XgridsSensorCatalogStream {
  stream_id: string;
  source_id?: string | null;
  semantic_channel_id?: string | null;
  label?: string | null;
  sensor_kind?: string | null;
  modality?: string | null;
  availability?: string | null;
  endpoint_label?: string | null;
  activation?: XgridsCameraPreviewActivation | null;
  delivery?: XgridsCameraPreviewDelivery | null;
  decode_status?: string | null;
  frame_id?: string | null;
  coordinate_convention?: string | null;
}

export interface XgridsSensorCatalog {
  schema_version?: string | null;
  revision?: string | null;
  streams?: XgridsSensorCatalogStream[];
}

export interface XgridsK1State {
  contract_version?: string | null;
  phase?: string | null;
  message?: string | null;
  devices?: BleDevice[];
  selected_device_id?: string | null;
  k1_ip?: string | null;
  connection_mode?: "bridge" | "quick-connect" | "direct-connect" | null;
  foxglove_ws_url?: string | null;
  foxglove_viewer_url?: string | null;
  rerun_grpc_url?: string | null;
  viewer_settings?: ViewerSettings | null;
  source_mode?: SourceMode | null;
  metrics?: XgridsK1Metrics;
  compatibility?: XgridsCompatibilityState | null;
  modeling_control_safety?: XgridsModelingControlSafety | null;
  application_control_execution?: XgridsApplicationControlExecution | null;
  application_control_session?: XgridsApplicationControlSession | null;
  device_ref?: XgridsDeviceRef | null;
  device_session?: XgridsDeviceSession | null;
  acquisition?: XgridsAcquisition | null;
  operations?: XgridsOperation[];
  last_operation?: XgridsOperation | null;
  sensor_catalog?: XgridsSensorCatalog | null;
  camera_preview?: XgridsCameraPreviewState | null;
}

export interface HealthResponse {
  ok?: boolean;
  status?: string;
  service?: string;
  version?: string;
}

export interface ScanRequest {
  duration_seconds?: number;
}

export interface CompatibilityAttestation {
  firmware_version: "3.0.2";
  topology: "direct-lan" | "device-ap" | "controller-hotspot";
  verification: "live-device-info";
}

export interface ConnectRequest {
  device_id: string;
  ssid?: string;
  password?: string;
  connection_mode: "bridge" | "quick-connect" | "direct-connect";
  compatibility_attestation: CompatibilityAttestation;
  operation_id?: string;
  idempotency_key?: string;
}

export interface PrepareAcquisitionRequest {
  project_name: string;
  mount_type: "handheld";
  gnss_mode: "none";
  host?: string;
  duration_seconds?: number;
  requested_streams?: RequestedStreamId[];
  evidence_policy?: "required" | "best-effort" | "disabled";
  compatibility_attestation: CompatibilityAttestation;
  operation_id?: string;
  idempotency_key?: string;
  deadline_seconds?: number;
}

export type RequestedStreamId =
  | "spatial.point-cloud.live"
  | "spatial.pose.live"
  | "device.modeling.live"
  | "device.status.live"
  | "device.heartbeat.live";

export interface StartAcquisitionRequest {
  acquisition_id: string;
  expected_state_revision?: number;
  operation_id?: string;
  idempotency_key?: string;
  deadline_seconds?: number;
  physical_acceptance?: OperatorPresenceConfirmation;
}

export interface StopAcquisitionRequest {
  acquisition_id: string;
  mode: "capture-only" | "graceful";
  operator_confirmed?: boolean;
  operation_id?: string;
  idempotency_key?: string;
  deadline_seconds?: number;
  physical_acceptance?: OperatorPresenceConfirmation;
}

export interface AbortAcquisitionRequest {
  acquisition_id: string;
  operation_id?: string;
  idempotency_key?: string;
  deadline_seconds?: number;
}

export interface CompatibilityLiveRequest {
  project_name: string;
  host?: string;
  duration_seconds?: number;
  compatibility_attestation: CompatibilityAttestation;
}

export interface ReplayRequest {
  path: string;
  speed?: number;
  loop?: boolean;
}

export interface SelectCameraPreviewRequest {
  source_id: string;
  device_session_id: string;
}

export interface StopCameraPreviewRequest {
  device_session_id: string;
  generation: number;
}

export interface ShadowApplicationControlArmRequest {
  operator_confirmed: true;
  lease_seconds?: number;
  timezone_name: string;
}

export interface OperatorPresenceConfirmation {
  operator_present: true;
  owner_controlled_device: true;
  lixelgo_closed: true;
  battery_storage_confirmed: true;
  expected_physical_state_confirmed: true;
}

export interface OpenApplicationControlSessionRequest
  extends OperatorPresenceConfirmation {
  timezone_name: string;
}

export interface EnterApplicationWorkspaceRequest {
  operator_confirmed: true;
}

export class ApiError extends Error {
  readonly status: number;
  readonly transportUnavailable: boolean;

  constructor(message: string, status = 0, transportUnavailable = false) {
    super(message);
    this.name = "ApiError";
    this.status = status;
    this.transportUnavailable = transportUnavailable;
  }
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function unwrapState(payload: unknown): XgridsK1State {
  const value = isRecord(payload) && isRecord(payload.state) ? payload.state : payload;

  if (!isRecord(value)) {
    throw new ApiError("Локальный сервер вернул некорректное состояние.");
  }

  return value as XgridsK1State;
}

async function requestJson(path: string, init?: RequestInit): Promise<unknown> {
  let response: Response;

  try {
    response = await fetch(path, {
      ...init,
      headers: {
        Accept: "application/json",
        ...(init?.body ? { "Content-Type": "application/json" } : {}),
        ...init?.headers,
      },
    });
  } catch {
    throw new ApiError(
      "Не удалось подключиться к локальному сервису устройства.",
      0,
      true,
    );
  }

  const bodyText = await response.text();
  let body: unknown;

  if (bodyText) {
    try {
      body = JSON.parse(bodyText) as unknown;
    } catch {
      body = bodyText;
    }
  }

  if (!response.ok) {
    const detail =
      isRecord(body) && typeof body.detail === "string"
        ? body.detail
        : typeof body === "string" && body.trim()
          ? body.trim()
          : `Запрос к API устройства завершился ошибкой HTTP ${response.status}.`;
    throw new ApiError(
      detail || `Запрос к API устройства завершился ошибкой HTTP ${response.status}.`,
      response.status,
    );
  }

  return body;
}

async function postState(path: string, body?: object): Promise<XgridsK1State> {
  const payload = await requestJson(path, {
    method: "POST",
    body: body ? JSON.stringify(body) : undefined,
  });

  if (payload === undefined) {
    return xgridsK1Api.getState();
  }

  return unwrapState(payload);
}

function invokeState(actionId: string, input: object = {}): Promise<XgridsK1State> {
  return postState(
    `/api/v1/device-plugins/${encodeURIComponent(PLUGIN_ID)}/actions/${encodeURIComponent(actionId)}`,
    { input },
  );
}

export const xgridsK1Api = {
  async getHealth(): Promise<HealthResponse> {
    const payload = await requestJson("/api/health");
    if (!isRecord(payload)) {
      throw new ApiError("Локальный сервер вернул некорректный ответ проверки.");
    }
    return payload as HealthResponse;
  },

  async getState(): Promise<XgridsK1State> {
    return invokeState(xgridsK1Actions.stateRead);
  },

  scanBle(body: ScanRequest = {}): Promise<XgridsK1State> {
    return invokeState(xgridsK1Actions.discoveryScan, body);
  },

  connect(body: ConnectRequest): Promise<XgridsK1State> {
    return invokeState(xgridsK1Actions.networkProvision, body);
  },

  prepareAcquisition(body: PrepareAcquisitionRequest): Promise<XgridsK1State> {
    return invokeState(xgridsK1Actions.acquisitionPrepare, body);
  },

  startAcquisition(body: StartAcquisitionRequest): Promise<XgridsK1State> {
    return invokeState(xgridsK1Actions.acquisitionStart, body);
  },

  stopAcquisition(body: StopAcquisitionRequest): Promise<XgridsK1State> {
    return invokeState(xgridsK1Actions.acquisitionStop, body);
  },

  abortAcquisition(body: AbortAcquisitionRequest): Promise<XgridsK1State> {
    return invokeState(xgridsK1Actions.acquisitionAbort, body);
  },

  startReplay(body: ReplayRequest): Promise<XgridsK1State> {
    return invokeState(xgridsK1Actions.streamStartReplay, body);
  },

  startLiveCompatibility(body: CompatibilityLiveRequest): Promise<XgridsK1State> {
    return invokeState(xgridsK1Actions.compatibilityStreamStartLive, body);
  },

  stopSessionCompatibility(): Promise<XgridsK1State> {
    return invokeState(xgridsK1Actions.compatibilityStreamStop);
  },

  selectCameraPreview(body: SelectCameraPreviewRequest): Promise<XgridsK1State> {
    return invokeState(xgridsK1Actions.cameraPreviewSelect, body);
  },

  stopCameraPreview(body: StopCameraPreviewRequest): Promise<XgridsK1State> {
    return invokeState(xgridsK1Actions.cameraPreviewStop, body);
  },

  updateViewerSettings(body: ViewerSettings): Promise<XgridsK1State> {
    return invokeState(xgridsK1Actions.viewerSettingsUpdate, body);
  },

  getShadowApplicationControlState(): Promise<XgridsK1State> {
    return invokeState(xgridsK1Actions.applicationControlShadowState);
  },

  armShadowApplicationControl(
    body: ShadowApplicationControlArmRequest,
  ): Promise<XgridsK1State> {
    return invokeState(xgridsK1Actions.applicationControlShadowArm, body);
  },

  disarmShadowApplicationControl(): Promise<XgridsK1State> {
    return invokeState(xgridsK1Actions.applicationControlShadowDisarm);
  },

  openApplicationControlSession(
    body: OpenApplicationControlSessionRequest,
  ): Promise<XgridsK1State> {
    return invokeState(xgridsK1Actions.applicationControlSessionOpen, body);
  },

  enterApplicationWorkspace(
    body: EnterApplicationWorkspaceRequest,
  ): Promise<XgridsK1State> {
    return invokeState(xgridsK1Actions.applicationControlWorkspaceEnter, body);
  },

  closeApplicationControlSession(): Promise<XgridsK1State> {
    return invokeState(xgridsK1Actions.applicationControlSessionClose);
  },
};

export type EventSocketStatus = "connecting" | "open" | "closed" | "error";

function eventSocketUrl(): string {
  const url = new URL(
    `/api/v1/device-plugins/${encodeURIComponent(PLUGIN_ID)}/events`,
    window.location.href,
  );
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
  return url.toString();
}

export function openEventSocket(
  onState: (state: XgridsK1State) => void,
  onStatus: (status: EventSocketStatus) => void,
): () => void {
  onStatus("connecting");
  const socket = new WebSocket(eventSocketUrl());

  socket.addEventListener("open", () => onStatus("open"));
  socket.addEventListener("message", (event) => {
    try {
      const payload = JSON.parse(String(event.data)) as unknown;
      onState(unwrapState(payload));
    } catch {
      // The REST poll remains authoritative if an unrelated event is received.
    }
  });
  socket.addEventListener("error", () => onStatus("error"));
  socket.addEventListener("close", () => onStatus("closed"));

  return () => socket.close(1000, "Пункт управления закрыт");
}
