import {
  ImageResult,
  PlatformService,
  PlatformCapabilities,
} from "../PlatformService";
import { logger } from "../../utils/logger";
import { QueryExecResult } from "@/interfaces/database";
import { BaseDatabaseService } from "./BaseDatabaseService";
// Dynamic import of initBackend to prevent worker context errors
import type {
  WorkerRequest,
  WorkerResponse,
  QueryRequest,
  ExecRequest,
  QueryResult,
  GetOneRowRequest,
} from "@/interfaces/worker-messages";

/**
 * Platform service implementation for web browser platform.
 * Implements the PlatformService interface with web-specific functionality.
 *
 * @remarks
 * This service provides web-based implementations for:
 * - Image capture using the browser's file input
 * - Image selection from local filesystem
 * - Image processing and conversion
 * - Database operations via worker thread messaging
 *
 * Note: File system operations are not available in the web platform
 * due to browser security restrictions. These methods throw appropriate errors.
 */
export class WebPlatformService
  extends BaseDatabaseService
  implements PlatformService
{
  private static instanceCount = 0; // Debug counter
  private worker: Worker | null = null;
  private workerReady = false;
  private workerInitPromise: Promise<void> | null = null;
  private pendingMessages = new Map<
    string,
    {
      resolve: (_value: unknown) => void;
      reject: (_reason: unknown) => void;
      timeout: NodeJS.Timeout;
    }
  >();
  private messageIdCounter = 0;
  private readonly messageTimeout = 30000; // 30 seconds

  constructor() {
    super();
    WebPlatformService.instanceCount++;

    logger.debug("[WebPlatformService] Initializing web platform service");

    // Only initialize SharedArrayBuffer setup for web platforms
    if (this.isWorker()) {
      logger.debug(
        "[WebPlatformService] Skipping initBackend call in worker context",
      );
      return;
    }

    // Initialize shared array buffer for main thread
    this.initSharedArrayBuffer();

    // Start worker initialization but don't await it in constructor
    this.workerInitPromise = this.initializeWorker();
  }

  /**
   * Initialize the SQL worker for database operations
   */
  private async initializeWorker(): Promise<void> {
    try {
      this.worker = new Worker(
        new URL("../../registerSQLWorker.js", import.meta.url),
        { type: "module" },
      );

      // This is required for Safari compatibility with nested workers
      // It installs a handler that proxies web worker creation through the main thread
      // CRITICAL: Only call initBackend from main thread, not from worker context
      const isMainThread = typeof window !== "undefined";
      if (isMainThread) {
        // We're in the main thread - safe to dynamically import and call initBackend
        try {
          const { initBackend } = await import(
            "absurd-sql/dist/indexeddb-main-thread"
          );
          initBackend(this.worker);
        } catch (error) {
          logger.error(
            "[WebPlatformService] Failed to import/call initBackend:",
            error,
          );
          throw error;
        }
      } else {
        // We're in a worker context - skip initBackend call
        // Use console for critical startup message to avoid circular dependency
        // eslint-disable-next-line no-console
        console.log(
          "[WebPlatformService] Skipping initBackend call in worker context",
        );
      }

      this.worker.onmessage = (event) => {
        this.handleWorkerMessage(event.data);
      };

      this.worker.onerror = (error) => {
        logger.error("[WebPlatformService] Worker error:", error);
        this.workerReady = false;
      };

      // Send ping to verify worker is ready
      await this.sendWorkerMessage({ type: "ping" });
      this.workerReady = true;
    } catch (error) {
      logger.error("[WebPlatformService] Failed to initialize worker:", error);
      this.workerReady = false;
      this.workerInitPromise = null;
      throw new Error("Failed to initialize database worker");
    }
  }

  /**
   * Handle messages received from the worker
   */
  private handleWorkerMessage(message: WorkerResponse): void {
    const { id, type } = message;

    // Handle absurd-sql internal messages (these are normal, don't log)
    if (!id && message.type?.startsWith("__absurd:")) {
      return; // Internal absurd-sql message, ignore silently
    }

    if (!id) {
      logger.warn("[WebPlatformService] Received message without ID:", message);
      return;
    }

    const pending = this.pendingMessages.get(id);
    if (!pending) {
      logger.warn(
        "[WebPlatformService] Received response for unknown message ID:",
        id,
      );
      return;
    }

    // Clear timeout and remove from pending
    clearTimeout(pending.timeout);
    this.pendingMessages.delete(id);

    switch (type) {
      case "success":
        pending.resolve(message.data);
        break;

      case "error": {
        const error = new Error(message.error.message);
        if (message.error.stack) {
          error.stack = message.error.stack;
        }
        pending.reject(error);
        break;
      }

      case "init-complete":
        pending.resolve(true);
        break;

      case "pong":
        pending.resolve(true);
        break;

      default:
        logger.warn("[WebPlatformService] Unknown response type:", type);
        pending.resolve(message);
        break;
    }
  }

  /**
   * Send a message to the worker and wait for response
   */
  private async sendWorkerMessage<T>(
    request: Omit<WorkerRequest, "id">,
  ): Promise<T> {
    if (!this.worker) {
      throw new Error("Worker not initialized");
    }

    const id = `msg_${++this.messageIdCounter}_${Date.now()}`;
    const fullRequest: WorkerRequest = { id, ...request } as WorkerRequest;

    return new Promise<T>((resolve, reject) => {
      const timeout = setTimeout(() => {
        this.pendingMessages.delete(id);
        reject(new Error(`Worker message timeout for ${request.type} (${id})`));
      }, this.messageTimeout);

      this.pendingMessages.set(id, {
        resolve: resolve as (value: unknown) => void,
        reject,
        timeout,
      });

      this.worker!.postMessage(fullRequest);
    });
  }

  /**
   * Wait for worker to be ready
   */
  private async ensureWorkerReady(): Promise<void> {
    // Wait for initial initialization to complete
    if (this.workerInitPromise) {
      await this.workerInitPromise;
    }

    if (this.workerReady) {
      return;
    }

    // Try to ping the worker if not ready
    try {
      await this.sendWorkerMessage<boolean>({ type: "ping" });
      this.workerReady = true;
    } catch (error) {
      logger.error("[WebPlatformService] Worker not ready:", error);
      throw new Error("Database worker not ready");
    }
  }

  /**
   * Gets the capabilities of the web platform
   * @returns Platform capabilities object
   */
  getCapabilities(): PlatformCapabilities {
    return {
      hasFileSystem: false,
      hasCamera: true, // Through file input with capture
      isMobile: /iPhone|iPad|iPod|Android/i.test(navigator.userAgent),
      isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent),
      hasFileDownload: true,
      needsFileHandlingInstructions: false,
      isNativeApp: false, // Web is not a native app
    };
  }

  /**
   * Not supported in web platform.
   * @param _path - Unused path parameter
   * @throws Error indicating file system access is not available
   */
  async readFile(_path: string): Promise<string> {
    throw new Error("File system access not available in web platform");
  }

  /**
   * Not supported in web platform.
   * @param _path - Unused path parameter
   * @param _content - Unused content parameter
   * @throws Error indicating file system access is not available
   */
  async writeFile(_path: string, _content: string): Promise<void> {
    throw new Error("File system access not available in web platform");
  }

  /**
   * Not supported in web platform.
   * @param _path - Unused path parameter
   * @throws Error indicating file system access is not available
   */
  async deleteFile(_path: string): Promise<void> {
    throw new Error("File system access not available in web platform");
  }

  /**
   * Not supported in web platform.
   * @param _directory - Unused directory parameter
   * @throws Error indicating file system access is not available
   */
  async listFiles(_directory: string): Promise<string[]> {
    throw new Error("File system access not available in web platform");
  }

  /**
   * Opens the device camera for photo capture on desktop browsers using getUserMedia.
   * On mobile browsers, uses file input with capture attribute.
   * Falls back to file input if getUserMedia is not available or fails.
   *
   * @returns Promise resolving to the captured image data
   */
  async takePicture(): Promise<ImageResult> {
    const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
    const hasGetUserMedia = !!(
      navigator.mediaDevices && navigator.mediaDevices.getUserMedia
    );

    // If on mobile, use file input with capture attribute (existing behavior)
    if (isMobile || !hasGetUserMedia) {
      return new Promise((resolve, reject) => {
        const input = document.createElement("input");
        input.type = "file";
        input.accept = "image/*";
        input.capture = "environment";

        input.onchange = async (e) => {
          const file = (e.target as HTMLInputElement).files?.[0];
          if (file) {
            try {
              const blob = await this.processImageFile(file);
              resolve({
                blob,
                fileName: file.name || "photo.jpg",
              });
            } catch (error) {
              logger.error("Error processing camera image:", error);
              reject(new Error("Failed to process camera image"));
            }
          } else {
            reject(new Error("No image captured"));
          }
        };

        input.click();
      });
    }

    // Desktop: Use getUserMedia for webcam capture
    return new Promise((resolve, reject) => {
      let stream: MediaStream | null = null;
      let video: HTMLVideoElement | null = null;
      let captureButton: HTMLButtonElement | null = null;
      let overlay: HTMLDivElement | null = null;
      const cleanup = () => {
        if (stream) {
          stream.getTracks().forEach((track) => track.stop());
        }
        if (video && video.parentNode) video.parentNode.removeChild(video);
        if (captureButton && captureButton.parentNode)
          captureButton.parentNode.removeChild(captureButton);
        if (overlay && overlay.parentNode)
          overlay.parentNode.removeChild(overlay);
      };

      // Move async operations inside Promise body
      navigator.mediaDevices
        .getUserMedia({
          video: { facingMode: "user" },
        })
        .then((mediaStream) => {
          stream = mediaStream;
          // Create overlay for video and button
          overlay = document.createElement("div");
          overlay.style.position = "fixed";
          overlay.style.top = "0";
          overlay.style.left = "0";
          overlay.style.width = "100vw";
          overlay.style.height = "100vh";
          overlay.style.background = "rgba(0,0,0,0.8)";
          overlay.style.display = "flex";
          overlay.style.flexDirection = "column";
          overlay.style.justifyContent = "center";
          overlay.style.alignItems = "center";
          overlay.style.zIndex = "9999";

          video = document.createElement("video");
          video.autoplay = true;
          video.playsInline = true;
          video.style.maxWidth = "90vw";
          video.style.maxHeight = "70vh";
          video.srcObject = stream;
          overlay.appendChild(video);

          captureButton = document.createElement("button");
          captureButton.textContent = "Capture Photo";
          captureButton.style.marginTop = "2rem";
          captureButton.style.padding = "1rem 2rem";
          captureButton.style.fontSize = "1.2rem";
          captureButton.style.background = "#2563eb";
          captureButton.style.color = "white";
          captureButton.style.border = "none";
          captureButton.style.borderRadius = "0.5rem";
          captureButton.style.cursor = "pointer";
          overlay.appendChild(captureButton);

          document.body.appendChild(overlay);

          captureButton.onclick = () => {
            try {
              // Create a canvas to capture the frame
              const canvas = document.createElement("canvas");
              canvas.width = video!.videoWidth;
              canvas.height = video!.videoHeight;
              const ctx = canvas.getContext("2d");
              ctx?.drawImage(video!, 0, 0, canvas.width, canvas.height);
              canvas.toBlob(
                (blob) => {
                  cleanup();
                  if (blob) {
                    resolve({
                      blob,
                      fileName: `photo_${Date.now()}.jpg`,
                    });
                  } else {
                    reject(new Error("Failed to capture image from webcam"));
                  }
                },
                "image/jpeg",
                0.95,
              );
            } catch (err) {
              cleanup();
              reject(err);
            }
          };
        })
        .catch((error) => {
          cleanup();
          logger.error("Error accessing webcam:", error);
          // Fallback to file input
          const input = document.createElement("input");
          input.type = "file";
          input.accept = "image/*";
          input.onchange = (e) => {
            const file = (e.target as HTMLInputElement).files?.[0];
            if (file) {
              this.processImageFile(file)
                .then((blob) => {
                  resolve({
                    blob,
                    fileName: file.name || "photo.jpg",
                  });
                })
                .catch((error) => {
                  logger.error("Error processing fallback image:", error);
                  reject(new Error("Failed to process fallback image"));
                });
            } else {
              reject(new Error("No image selected"));
            }
          };
          input.click();
        });
    });
  }

  /**
   * Opens a file input dialog for selecting an image file.
   * Creates a temporary file input element to access local files.
   *
   * @returns Promise resolving to the selected image data
   * @throws Error if image processing fails or no image is selected
   *
   * @remarks
   * Allows selection of any image file type.
   * Processes the selected image to ensure consistent format.
   */
  async pickImage(): Promise<ImageResult> {
    return new Promise((resolve, reject) => {
      const input = document.createElement("input");
      input.type = "file";
      input.accept = "image/*";

      input.onchange = async (e) => {
        const file = (e.target as HTMLInputElement).files?.[0];
        if (file) {
          try {
            const blob = await this.processImageFile(file);
            resolve({
              blob,
              fileName: file.name || "photo.jpg",
            });
          } catch (error) {
            logger.error("Error processing picked image:", error);
            reject(new Error("Failed to process picked image"));
          }
        } else {
          reject(new Error("No image selected"));
        }
      };

      input.click();
    });
  }

  /**
   * Processes an image file to ensure consistent format.
   * Converts the file to a data URL and then to a Blob.
   *
   * @param file - The image File object to process
   * @returns Promise resolving to processed image Blob
   * @throws Error if file reading or conversion fails
   *
   * @remarks
   * This method ensures consistent image format across different
   * input sources by converting through data URL to Blob.
   */
  private async processImageFile(file: File): Promise<Blob> {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = (event) => {
        const dataUrl = event.target?.result as string;
        // Convert to blob to ensure consistent format
        fetch(dataUrl)
          .then((res) => res.blob())
          .then((blob) => resolve(blob))
          .catch((error) => {
            logger.error("Error converting data URL to blob:", error);
            reject(error);
          });
      };
      reader.onerror = (error) => {
        logger.error("Error reading file:", error);
        reject(error);
      };
      reader.readAsDataURL(file);
    });
  }

  /**
   * Checks if running on Capacitor platform.
   * @returns false, as this is not Capacitor
   */
  isCapacitor(): boolean {
    return false;
  }

  /**
   * Checks if running on Electron platform.
   * @returns false, as this is not Electron
   */
  isElectron(): boolean {
    return false;
  }

  /**
   * Checks if running on web platform.
   * @returns true, as this is the web implementation
   */
  isWeb(): boolean {
    return true;
  }

  /**
   * Handles deep link URLs in the web platform.
   * Deep links are handled through URL parameters in the web environment.
   *
   * @param _url - The deep link URL to handle (unused in web implementation)
   * @returns Promise that resolves immediately as web handles URLs naturally
   */
  async handleDeepLink(_url: string): Promise<void> {
    // Web platform can handle deep links through URL parameters
    return Promise.resolve();
  }

  /**
   * Downloads a file in the web platform using blob URLs and download links.
   * Creates a temporary download link and triggers the browser's download mechanism.
   * @param fileName - The name of the file to download
   * @param content - The content to write to the file
   * @returns Promise that resolves when the download is initiated
   */
  async writeAndShareFile(fileName: string, content: string): Promise<void> {
    try {
      // Create a blob with the content
      const blob = new Blob([content], { type: "application/json" });

      // Create a temporary download link
      const url = URL.createObjectURL(blob);
      const downloadLink = document.createElement("a");
      downloadLink.href = url;
      downloadLink.download = fileName;
      downloadLink.style.display = "none";

      // Add to DOM, click, and remove
      document.body.appendChild(downloadLink);
      downloadLink.click();
      document.body.removeChild(downloadLink);

      // Clean up the object URL after a short delay
      setTimeout(() => URL.revokeObjectURL(url), 1000);

      logger.log("[WebPlatformService] File download initiated:", fileName);
    } catch (error) {
      logger.error("[WebPlatformService] Error downloading file:", error);
      throw new Error(
        `Failed to download file: ${error instanceof Error ? error.message : "Unknown error"}`,
      );
    }
  }

  /**
   * @see PlatformService.dbQuery
   */
  async dbQuery(
    sql: string,
    params?: unknown[],
  ): Promise<QueryExecResult | undefined> {
    await this.ensureWorkerReady();
    return this.sendWorkerMessage<QueryResult>({
      type: "query",
      sql,
      params,
    } as QueryRequest).then((result) => result.result[0]);
  }

  /**
   * @see PlatformService.dbExec
   */
  async dbExec(
    sql: string,
    params?: unknown[],
  ): Promise<{ changes: number; lastId?: number }> {
    await this.ensureWorkerReady();
    return this.sendWorkerMessage<{ changes: number; lastId?: number }>({
      type: "exec",
      sql,
      params,
    } as ExecRequest);
  }

  async dbGetOneRow(
    sql: string,
    params?: unknown[],
  ): Promise<unknown[] | undefined> {
    await this.ensureWorkerReady();
    return this.sendWorkerMessage<unknown[] | undefined>({
      type: "getOneRow",
      sql,
      params,
    } as GetOneRowRequest);
  }

  /**
   * @see PlatformService.dbRawQuery
   */
  async dbRawQuery(
    sql: string,
    params?: unknown[],
  ): Promise<unknown | undefined> {
    // This class doesn't post-process the result, so we can just use it.
    return this.dbQuery(sql, params);
  }

  /**
   * Rotates the camera between front and back cameras.
   * @returns Promise that resolves when the camera is rotated
   * @throws Error indicating camera rotation is not implemented in web platform
   */
  async rotateCamera(): Promise<void> {
    throw new Error("Camera rotation not implemented in web platform");
  }

  /**
   * Checks if running in a worker context
   */
  private isWorker(): boolean {
    return typeof window === "undefined";
  }

  /**
   * Initialize SharedArrayBuffer setup (handled by initBackend in initializeWorker)
   */
  private initSharedArrayBuffer(): void {
    // SharedArrayBuffer initialization is handled by initBackend call in initializeWorker
  }

  // Database utility methods - inherited from BaseDatabaseService
  // generateInsertStatement, updateDefaultSettings, updateActiveDid,
  // getActiveIdentity, insertNewDidIntoSettings, updateDidSpecificSettings,
  // retrieveSettingsForActiveAccount are all inherited from BaseDatabaseService
}
