import {
  QRScannerService,
  ScanListener,
  QRScannerOptions,
  CameraState,
  CameraStateListener,
} from "./types";
import { logger } from "@/utils/logger";
import { EventEmitter } from "events";
import jsQR from "jsqr";

// Build identifier to help distinguish between builds
const BUILD_ID = `build-${Date.now()}`;

export class WebInlineQRScanner implements QRScannerService {
  private scanListener: ScanListener | null = null;
  private isScanning = false;
  private stream: MediaStream | null = null;
  private events = new EventEmitter();
  private canvas: HTMLCanvasElement | null = null;
  private context: CanvasRenderingContext2D | null = null;
  private video: HTMLVideoElement | null = null;
  private animationFrameId: number | null = null;
  private scanAttempts = 0;
  private lastScanTime = 0;
  private readonly id: string;
  private readonly TARGET_FPS = 15; // Target 15 FPS for scanning
  private readonly FRAME_INTERVAL = 1000 / 15; // ~67ms between frames
  private lastFrameTime = 0;
  private cameraStateListeners: Set<CameraStateListener> = new Set();
  private currentState: CameraState = "off";
  private currentStateMessage?: string;
  private options: QRScannerOptions;

  constructor(options?: QRScannerOptions) {
    // Generate a short random ID for this scanner instance
    this.id = Math.random().toString(36).substring(2, 8).toUpperCase();
    this.options = options ?? {};
    logger.error(
      `[WebInlineQRScanner:${this.id}] Initializing scanner with options:`,
      {
        ...this.options,
        buildId: BUILD_ID,
        targetFps: this.TARGET_FPS,
      },
    );
    // Create canvas and video elements
    this.canvas = document.createElement("canvas");
    this.context = this.canvas.getContext("2d", { willReadFrequently: true });
    this.video = document.createElement("video");
    this.video.setAttribute("playsinline", "true"); // Required for iOS
    logger.error(
      `[WebInlineQRScanner:${this.id}] DOM elements created successfully`,
    );
  }

  private updateCameraState(state: CameraState, message?: string) {
    this.currentState = state;
    this.currentStateMessage = message;
    this.cameraStateListeners.forEach((listener) => {
      try {
        listener.onStateChange(state, message);
        logger.info(
          `[WebInlineQRScanner:${this.id}] Camera state changed to: ${state}`,
          {
            state,
            message,
          },
        );
      } catch (error) {
        logger.error(
          `[WebInlineQRScanner:${this.id}] Error in camera state listener:`,
          error,
        );
      }
    });
  }

  addCameraStateListener(listener: CameraStateListener): void {
    this.cameraStateListeners.add(listener);
    // Immediately notify the new listener of current state
    listener.onStateChange(this.currentState, this.currentStateMessage);
  }

  removeCameraStateListener(listener: CameraStateListener): void {
    this.cameraStateListeners.delete(listener);
  }

  async checkPermissions(): Promise<boolean> {
    try {
      this.updateCameraState("initializing", "Checking camera permissions...");
      logger.error(
        `[WebInlineQRScanner:${this.id}] Checking camera permissions...`,
      );

      // First try the Permissions API if available
      if (navigator.permissions && navigator.permissions.query) {
        try {
          const permissions = await navigator.permissions.query({
            name: "camera" as PermissionName,
          });
          logger.error(
            `[WebInlineQRScanner:${this.id}] Permission state from Permissions API:`,
            permissions.state,
          );
          if (permissions.state === "granted") {
            this.updateCameraState("ready", "Camera permissions granted");
            return true;
          }
        } catch (permError) {
          // Permissions API might not be supported, continue with getUserMedia check
          logger.error(
            `[WebInlineQRScanner:${this.id}] Permissions API not supported:`,
            permError,
          );
        }
      }

      // If Permissions API is not available or didn't return granted,
      // try a test getUserMedia call
      try {
        const testStream = await navigator.mediaDevices.getUserMedia({
          video: true,
        });
        // If we get here, we have permission
        testStream.getTracks().forEach((track) => track.stop());
        this.updateCameraState("ready", "Camera permissions granted");
        return true;
      } catch (mediaError) {
        const error = mediaError as Error;
        logger.error(
          `[WebInlineQRScanner:${this.id}] getUserMedia test failed:`,
          {
            name: error.name,
            message: error.message,
          },
        );

        if (
          error.name === "NotAllowedError" ||
          error.name === "PermissionDeniedError"
        ) {
          this.updateCameraState("permission_denied", "Camera access denied");
          return false;
        }
        // For other errors, we'll try requesting permissions explicitly
        return false;
      }
    } catch (error) {
      logger.error(
        `[WebInlineQRScanner:${this.id}] Error checking camera permissions:`,
        {
          error: error instanceof Error ? error.message : String(error),
          stack: error instanceof Error ? error.stack : undefined,
        },
      );
      this.updateCameraState("error", "Error checking camera permissions");
      return false;
    }
  }

  async requestPermissions(): Promise<boolean> {
    try {
      this.updateCameraState(
        "initializing",
        "Requesting camera permissions...",
      );
      logger.error(
        `[WebInlineQRScanner:${this.id}] Requesting camera permissions...`,
      );

      // First check if we have any video devices
      const devices = await navigator.mediaDevices.enumerateDevices();
      const videoDevices = devices.filter(
        (device) => device.kind === "videoinput",
      );

      logger.error(`[WebInlineQRScanner:${this.id}] Found video devices:`, {
        count: videoDevices.length,
        devices: videoDevices.map((d) => ({ id: d.deviceId, label: d.label })),
        userAgent: navigator.userAgent,
      });

      if (videoDevices.length === 0) {
        logger.error(`[WebInlineQRScanner:${this.id}] No video devices found`);
        this.updateCameraState("not_found", "No camera found on this device");
        throw new Error("No camera found on this device");
      }

      // Try to get a stream with specific constraints
      logger.error(
        `[WebInlineQRScanner:${this.id}] Requesting camera stream with constraints:`,
        {
          facingMode: "environment",
          width: { ideal: 1280 },
          height: { ideal: 720 },
        },
      );

      try {
        const stream = await navigator.mediaDevices.getUserMedia({
          video: {
            facingMode: "environment",
            width: { ideal: 1280 },
            height: { ideal: 720 },
          },
        });

        this.updateCameraState("ready", "Camera permissions granted");

        // Stop the test stream immediately
        stream.getTracks().forEach((track) => {
          logger.error(`[WebInlineQRScanner:${this.id}] Stopping test track:`, {
            kind: track.kind,
            label: track.label,
            readyState: track.readyState,
          });
          track.stop();
        });
        return true;
      } catch (mediaError) {
        const error = mediaError as Error;
        logger.error(
          `[WebInlineQRScanner:${this.id}] Error requesting camera access:`,
          {
            name: error.name,
            message: error.message,
            userAgent: navigator.userAgent,
          },
        );

        // Update state based on error type
        if (
          error.name === "NotFoundError" ||
          error.name === "DevicesNotFoundError"
        ) {
          this.updateCameraState("not_found", "No camera found on this device");
          throw new Error("No camera found on this device");
        } else if (
          error.name === "NotAllowedError" ||
          error.name === "PermissionDeniedError"
        ) {
          this.updateCameraState("permission_denied", "Camera access denied");
          throw new Error(
            "Camera access denied. Please grant camera permission and try again",
          );
        } else if (
          error.name === "NotReadableError" ||
          error.name === "TrackStartError"
        ) {
          this.updateCameraState(
            "in_use",
            "Camera is in use by another application",
          );
          throw new Error("Camera is in use by another application");
        } else {
          this.updateCameraState("error", error.message);
          throw new Error(`Camera error: ${error.message}`);
        }
      }
    } catch (error) {
      const wrappedError =
        error instanceof Error ? error : new Error(String(error));
      logger.error(
        `[WebInlineQRScanner:${this.id}] Error in requestPermissions:`,
        {
          error: wrappedError.message,
          stack: wrappedError.stack,
          userAgent: navigator.userAgent,
        },
      );
      throw wrappedError;
    }
  }

  async isSupported(): Promise<boolean> {
    try {
      logger.error(
        `[WebInlineQRScanner:${this.id}] Checking browser support...`,
      );
      // Check for secure context first
      if (!window.isSecureContext) {
        logger.error(
          `[WebInlineQRScanner:${this.id}] Camera access requires HTTPS (secure context)`,
        );
        return false;
      }

      // Check for camera API support
      if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
        logger.error(
          `[WebInlineQRScanner:${this.id}] Camera API not supported in this browser`,
        );
        return false;
      }

      // Check if we have any video devices
      const devices = await navigator.mediaDevices.enumerateDevices();
      const hasVideoDevices = devices.some(
        (device) => device.kind === "videoinput",
      );

      logger.error(`[WebInlineQRScanner:${this.id}] Device support check:`, {
        hasSecureContext: window.isSecureContext,
        hasMediaDevices: !!navigator.mediaDevices,
        hasGetUserMedia: !!navigator.mediaDevices?.getUserMedia,
        hasVideoDevices,
        deviceCount: devices.length,
      });

      if (!hasVideoDevices) {
        logger.error(`[WebInlineQRScanner:${this.id}] No video devices found`);
        return false;
      }

      return true;
    } catch (error) {
      logger.error(
        `[WebInlineQRScanner:${this.id}] Error checking camera support:`,
        {
          error: error instanceof Error ? error.message : String(error),
          stack: error instanceof Error ? error.stack : undefined,
        },
      );
      return false;
    }
  }

  private async scanQRCode(): Promise<void> {
    if (!this.video || !this.canvas || !this.context || !this.stream) {
      logger.error(
        `[WebInlineQRScanner:${this.id}] Cannot scan: missing required elements`,
        {
          hasVideo: !!this.video,
          hasCanvas: !!this.canvas,
          hasContext: !!this.context,
          hasStream: !!this.stream,
        },
      );
      return;
    }

    try {
      const now = Date.now();
      const timeSinceLastFrame = now - this.lastFrameTime;

      // Throttle frame processing to target FPS
      if (timeSinceLastFrame < this.FRAME_INTERVAL) {
        this.animationFrameId = requestAnimationFrame(() => this.scanQRCode());
        return;
      }

      this.lastFrameTime = now;

      // Set canvas dimensions to match video
      this.canvas.width = this.video.videoWidth;
      this.canvas.height = this.video.videoHeight;

      // Draw video frame to canvas
      this.context.drawImage(
        this.video,
        0,
        0,
        this.canvas.width,
        this.canvas.height,
      );

      // Get image data from canvas
      const imageData = this.context.getImageData(
        0,
        0,
        this.canvas.width,
        this.canvas.height,
      );

      // Increment scan attempts
      this.scanAttempts++;
      const timeSinceLastScan = now - this.lastScanTime;

      // Log scan attempt every 100 frames or 1 second
      if (this.scanAttempts % 100 === 0 || timeSinceLastScan >= 1000) {
        logger.error(`[WebInlineQRScanner:${this.id}] Scanning frame:`, {
          attempt: this.scanAttempts,
          dimensions: {
            width: this.canvas.width,
            height: this.canvas.height,
          },
          fps: Math.round(1000 / timeSinceLastScan),
          imageDataSize: imageData.data.length,
          imageDataWidth: imageData.width,
          imageDataHeight: imageData.height,
          timeSinceLastFrame,
          targetFPS: this.TARGET_FPS,
        });
        this.lastScanTime = now;
      }

      // Scan for QR code
      const code = jsQR(imageData.data, imageData.width, imageData.height, {
        inversionAttempts: "attemptBoth", // Try both normal and inverted
      });

      if (code) {
        // Check if the QR code is blurry by examining the location points
        const { topRightCorner, topLeftCorner, bottomLeftCorner } =
          code.location;
        const width = Math.sqrt(
          Math.pow(topRightCorner.x - topLeftCorner.x, 2) +
            Math.pow(topRightCorner.y - topLeftCorner.y, 2),
        );
        const height = Math.sqrt(
          Math.pow(bottomLeftCorner.x - topLeftCorner.x, 2) +
            Math.pow(bottomLeftCorner.y - topLeftCorner.y, 2),
        );

        // Adjust minimum size based on canvas dimensions
        const minSize = Math.min(this.canvas.width, this.canvas.height) * 0.1; // 10% of the smaller dimension
        const isBlurry =
          width < minSize ||
          height < minSize ||
          !code.data ||
          code.data.length === 0;

        logger.error(`[WebInlineQRScanner:${this.id}] QR Code detected:`, {
          data: code.data,
          location: code.location,
          attempts: this.scanAttempts,
          isBlurry,
          dimensions: {
            width,
            height,
            minSize,
            canvasWidth: this.canvas.width,
            canvasHeight: this.canvas.height,
            relativeWidth: width / this.canvas.width,
            relativeHeight: height / this.canvas.height,
          },
          corners: {
            topLeft: topLeftCorner,
            topRight: topRightCorner,
            bottomLeft: bottomLeftCorner,
          },
        });

        if (isBlurry) {
          if (this.scanListener?.onError) {
            this.scanListener.onError(
              new Error(
                "QR code detected but too blurry to read. Please hold the camera steady and ensure the QR code is well-lit.",
              ),
            );
          }
          // Continue scanning if QR code is blurry
          this.animationFrameId = requestAnimationFrame(() =>
            this.scanQRCode(),
          );
          return;
        }

        if (this.scanListener?.onScan) {
          this.scanListener.onScan(code.data);
        }
        // Stop scanning after successful detection
        await this.stopScan();
        return;
      }

      // Continue scanning if no QR code found
      this.animationFrameId = requestAnimationFrame(() => this.scanQRCode());
    } catch (error) {
      logger.error(`[WebInlineQRScanner:${this.id}] Error scanning QR code:`, {
        error: error instanceof Error ? error.message : String(error),
        stack: error instanceof Error ? error.stack : undefined,
        attempt: this.scanAttempts,
        videoState: this.video
          ? {
              readyState: this.video.readyState,
              paused: this.video.paused,
              ended: this.video.ended,
              width: this.video.videoWidth,
              height: this.video.videoHeight,
            }
          : null,
        canvasState: this.canvas
          ? {
              width: this.canvas.width,
              height: this.canvas.height,
            }
          : null,
      });
      if (this.scanListener?.onError) {
        this.scanListener.onError(
          error instanceof Error ? error : new Error(String(error)),
        );
      }
    }
  }

  async startScan(options?: QRScannerOptions): Promise<void> {
    if (this.isScanning) {
      logger.error(`[WebInlineQRScanner:${this.id}] Scanner already running`);
      return;
    }

    // Update options if provided
    if (options) {
      this.options = { ...this.options, ...options };
    }

    try {
      this.isScanning = true;
      this.scanAttempts = 0;
      this.lastScanTime = Date.now();
      this.updateCameraState("initializing", "Starting camera...");
      logger.error(
        `[WebInlineQRScanner:${this.id}] Starting scan with options:`,
        this.options,
      );

      // Get camera stream with options
      logger.error(
        `[WebInlineQRScanner:${this.id}] Requesting camera stream...`,
      );
      this.stream = await navigator.mediaDevices.getUserMedia({
        video: {
          facingMode: this.options.camera === "front" ? "user" : "environment",
          width: { ideal: 1280 },
          height: { ideal: 720 },
        },
      });

      this.updateCameraState("active", "Camera is active");

      logger.error(`[WebInlineQRScanner:${this.id}] Camera stream obtained:`, {
        tracks: this.stream.getTracks().map((t) => ({
          kind: t.kind,
          label: t.label,
          readyState: t.readyState,
        })),
        options: this.options,
      });

      // Set up video element
      if (this.video) {
        this.video.srcObject = this.stream;
        // Only show preview if showPreview is true
        if (this.options.showPreview) {
          this.video.style.display = "block";
        } else {
          this.video.style.display = "none";
        }
        await this.video.play();
        logger.error(
          `[WebInlineQRScanner:${this.id}] Video element started playing`,
        );
      }

      // Emit stream to component
      this.events.emit("stream", this.stream);
      logger.error(`[WebInlineQRScanner:${this.id}] Stream event emitted`);

      // Start QR code scanning
      this.scanQRCode();
    } catch (error) {
      this.isScanning = false;
      const wrappedError =
        error instanceof Error ? error : new Error(String(error));

      // Update state based on error type
      if (
        wrappedError.name === "NotReadableError" ||
        wrappedError.name === "TrackStartError"
      ) {
        this.updateCameraState(
          "in_use",
          "Camera is in use by another application",
        );
      } else {
        this.updateCameraState("error", wrappedError.message);
      }

      if (this.scanListener?.onError) {
        this.scanListener.onError(wrappedError);
      }
      throw wrappedError;
    }
  }

  async stopScan(): Promise<void> {
    if (!this.isScanning) {
      logger.error(
        `[WebInlineQRScanner:${this.id}] Scanner not running, nothing to stop`,
      );
      return;
    }

    try {
      logger.error(`[WebInlineQRScanner:${this.id}] Stopping scan`, {
        scanAttempts: this.scanAttempts,
        duration: Date.now() - this.lastScanTime,
      });

      // Stop animation frame
      if (this.animationFrameId !== null) {
        cancelAnimationFrame(this.animationFrameId);
        this.animationFrameId = null;
        logger.error(
          `[WebInlineQRScanner:${this.id}] Animation frame cancelled`,
        );
      }

      // Stop video
      if (this.video) {
        this.video.pause();
        this.video.srcObject = null;
        logger.error(`[WebInlineQRScanner:${this.id}] Video element stopped`);
      }

      // Stop all tracks in the stream
      if (this.stream) {
        this.stream.getTracks().forEach((track) => {
          logger.error(`[WebInlineQRScanner:${this.id}] Stopping track:`, {
            kind: track.kind,
            label: track.label,
            readyState: track.readyState,
          });
          track.stop();
        });
        this.stream = null;
      }

      // Emit stream stopped event
      this.events.emit("stream", null);
      logger.error(
        `[WebInlineQRScanner:${this.id}] Stream stopped event emitted`,
      );
    } catch (error) {
      logger.error(
        `[WebInlineQRScanner:${this.id}] Error stopping scan:`,
        error,
      );
      this.updateCameraState("error", "Error stopping camera");
      throw error;
    } finally {
      this.isScanning = false;
      logger.error(`[WebInlineQRScanner:${this.id}] Scan stopped successfully`);
    }
  }

  addListener(listener: ScanListener): void {
    logger.error(`[WebInlineQRScanner:${this.id}] Adding scan listener`);
    this.scanListener = listener;
  }

  onStream(callback: (stream: MediaStream | null) => void): void {
    logger.error(
      `[WebInlineQRScanner:${this.id}] Adding stream event listener`,
    );
    this.events.on("stream", callback);
  }

  async cleanup(): Promise<void> {
    try {
      logger.error(`[WebInlineQRScanner:${this.id}] Starting cleanup`);
      await this.stopScan();
      this.events.removeAllListeners();
      logger.error(`[WebInlineQRScanner:${this.id}] Event listeners removed`);

      // Clean up DOM elements
      if (this.video) {
        this.video.remove();
        this.video = null;
        logger.error(`[WebInlineQRScanner:${this.id}] Video element removed`);
      }
      if (this.canvas) {
        this.canvas.remove();
        this.canvas = null;
        logger.error(`[WebInlineQRScanner:${this.id}] Canvas element removed`);
      }
      this.context = null;
      logger.error(
        `[WebInlineQRScanner:${this.id}] Cleanup completed successfully`,
      );
    } catch (error) {
      logger.error(
        `[WebInlineQRScanner:${this.id}] Error during cleanup:`,
        error,
      );
      this.updateCameraState("error", "Error during cleanup");
      throw error;
    }
  }
}
