import { Filesystem, Directory, Encoding } from "@capacitor/filesystem";
import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
import { Share } from "@capacitor/share";
import {
  SQLiteConnection,
  SQLiteDBConnection,
  CapacitorSQLite,
  capSQLiteChanges,
  DBSQLiteValues,
} from "@capacitor-community/sqlite";

import { runMigrations } from "@/db-sql/migration";
import { QueryExecResult } from "@/interfaces/database";
import {
  ImageResult,
  PlatformService,
  PlatformCapabilities,
} from "../PlatformService";
import { logger } from "../../utils/logger";

interface QueuedOperation {
  type: "run" | "query";
  sql: string;
  params: unknown[];
  resolve: (value: unknown) => void;
  reject: (reason: unknown) => void;
}

/**
 * Platform service implementation for Capacitor (mobile) platform.
 * Provides native mobile functionality through Capacitor plugins for:
 * - File system operations
 * - Camera and image picker
 * - Platform-specific features
 * - SQLite database operations
 */
export class CapacitorPlatformService implements PlatformService {
  private sqlite: SQLiteConnection;
  private db: SQLiteDBConnection | null = null;
  private dbName = "timesafari.sqlite";
  private initialized = false;
  private initializationPromise: Promise<void> | null = null;
  private operationQueue: Array<QueuedOperation> = [];
  private isProcessingQueue: boolean = false;

  constructor() {
    this.sqlite = new SQLiteConnection(CapacitorSQLite);
  }

  private async initializeDatabase(): Promise<void> {
    // If already initialized, return immediately
    if (this.initialized) {
      return;
    }

    // If initialization is in progress, wait for it
    if (this.initializationPromise) {
      return this.initializationPromise;
    }

    // Start initialization
    this.initializationPromise = this._initialize();
    try {
      await this.initializationPromise;
    } catch (error) {
      logger.error(
        "[CapacitorPlatformService] Initialize method failed:",
        error,
      );
      this.initializationPromise = null; // Reset on failure
      throw error;
    }
  }

  private async _initialize(): Promise<void> {
    if (this.initialized) {
      return;
    }

    try {
      // Create/Open database
      this.db = await this.sqlite.createConnection(
        this.dbName,
        false,
        "no-encryption",
        1,
        false,
      );

      await this.db.open();

      // Set journal mode to WAL for better performance
      // await this.db.execute("PRAGMA journal_mode=WAL;");

      // Run migrations
      await this.runCapacitorMigrations();

      this.initialized = true;
      logger.log(
        "[CapacitorPlatformService] SQLite database initialized successfully",
      );

      // Start processing the queue after initialization
      this.processQueue();
    } catch (error) {
      logger.error(
        "[CapacitorPlatformService] Error initializing SQLite database:",
        error,
      );
      throw new Error(
        "[CapacitorPlatformService] Failed to initialize database",
      );
    }
  }

  private async processQueue(): Promise<void> {
    if (this.isProcessingQueue || !this.initialized || !this.db) {
      return;
    }

    this.isProcessingQueue = true;

    while (this.operationQueue.length > 0) {
      const operation = this.operationQueue.shift();
      if (!operation) continue;

      try {
        let result: unknown;
        switch (operation.type) {
          case "run": {
            const runResult = await this.db.run(
              operation.sql,
              operation.params,
            );
            result = {
              changes: runResult.changes?.changes || 0,
              lastId: runResult.changes?.lastId,
            };
            break;
          }
          case "query": {
            const queryResult = await this.db.query(
              operation.sql,
              operation.params,
            );
            result = {
              columns: Object.keys(queryResult.values?.[0] || {}),
              values: (queryResult.values || []).map((row) =>
                Object.values(row),
              ),
            };
            break;
          }
        }
        operation.resolve(result);
      } catch (error) {
        logger.error(
          "[CapacitorPlatformService] Error while processing SQL queue:",
          error,
        );
        operation.reject(error);
      }
    }

    this.isProcessingQueue = false;
  }

  private async queueOperation<R>(
    type: QueuedOperation["type"],
    sql: string,
    params: unknown[] = [],
  ): Promise<R> {
    return new Promise<R>((resolve, reject) => {
      const operation: QueuedOperation = {
        type,
        sql,
        params,
        resolve: (value: unknown) => resolve(value as R),
        reject,
      };
      this.operationQueue.push(operation);

      // If we're already initialized, start processing the queue
      if (this.initialized && this.db) {
        this.processQueue();
      }
    });
  }

  private async waitForInitialization(): Promise<void> {
    // If we have an initialization promise, wait for it
    if (this.initializationPromise) {
      await this.initializationPromise;
      return;
    }

    // If not initialized and no promise, start initialization
    if (!this.initialized) {
      await this.initializeDatabase();
      return;
    }

    // If initialized but no db, something went wrong
    if (!this.db) {
      logger.error(
        "[CapacitorPlatformService] Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null",
      );
      throw new Error(
        "[CapacitorPlatformService] The database could not be initialized. We recommend you restart or reinstall.",
      );
    }
  }

  private async runCapacitorMigrations(): Promise<void> {
    if (!this.db) {
      throw new Error("Database not initialized");
    }

    const sqlExec: (sql: string) => Promise<capSQLiteChanges> =
      this.db.execute.bind(this.db);
    const sqlQuery: (sql: string) => Promise<DBSQLiteValues> =
      this.db.query.bind(this.db);
    const extractMigrationNames: (result: DBSQLiteValues) => Set<string> = (
      result,
    ) => {
      const names =
        result.values?.map((row: { name: string }) => row.name) || [];
      return new Set(names);
    };
    runMigrations(sqlExec, sqlQuery, extractMigrationNames);
  }

  /**
   * Gets the capabilities of the Capacitor platform
   * @returns Platform capabilities object
   */
  getCapabilities(): PlatformCapabilities {
    return {
      hasFileSystem: true,
      hasCamera: true,
      isMobile: true,
      isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent),
      hasFileDownload: false,
      needsFileHandlingInstructions: true,
    };
  }

  /**
   * Checks and requests storage permissions if needed
   * @returns Promise that resolves when permissions are granted
   * @throws Error if permissions are denied
   */
  private async checkStoragePermissions(): Promise<void> {
    try {
      const logData = {
        platform: this.getCapabilities().isIOS ? "iOS" : "Android",
        timestamp: new Date().toISOString(),
      };
      logger.log(
        "Checking storage permissions",
        JSON.stringify(logData, null, 2),
      );

      if (this.getCapabilities().isIOS) {
        // iOS uses different permission model
        return;
      }

      // Try to access a test directory to check permissions
      try {
        await Filesystem.stat({
          path: "/storage/emulated/0/Download",
          directory: Directory.Documents,
        });
        logger.log(
          "Storage permissions already granted",
          JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
        );
        return;
      } catch (error: unknown) {
        const err = error as Error;
        const errorLogData = {
          error: {
            message: err.message,
            name: err.name,
            stack: err.stack,
          },
          timestamp: new Date().toISOString(),
        };

        // "File does not exist" is expected and not a permission error
        if (err.message === "File does not exist") {
          logger.log(
            "Directory does not exist (expected), proceeding with write",
            JSON.stringify(errorLogData, null, 2),
          );
          return;
        }

        // Check for actual permission errors
        if (
          err.message.includes("permission") ||
          err.message.includes("access")
        ) {
          logger.log(
            "Permission check failed, requesting permissions",
            JSON.stringify(errorLogData, null, 2),
          );

          // The Filesystem plugin will automatically request permissions when needed
          // We just need to try the operation again
          try {
            await Filesystem.stat({
              path: "/storage/emulated/0/Download",
              directory: Directory.Documents,
            });
            logger.log(
              "Storage permissions granted after request",
              JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
            );
            return;
          } catch (retryError: unknown) {
            const retryErr = retryError as Error;
            throw new Error(
              `Failed to obtain storage permissions: ${retryErr.message}`,
            );
          }
        }

        // For any other error, log it but don't treat as permission error
        logger.log(
          "Unexpected error during permission check",
          JSON.stringify(errorLogData, null, 2),
        );
        return;
      }
    } catch (error: unknown) {
      const err = error as Error;
      const errorLogData = {
        error: {
          message: err.message,
          name: err.name,
          stack: err.stack,
        },
        timestamp: new Date().toISOString(),
      };
      logger.error(
        "Error checking/requesting permissions",
        JSON.stringify(errorLogData, null, 2),
      );
      throw new Error(`Failed to obtain storage permissions: ${err.message}`);
    }
  }

  /**
   * Reads a file from the app's data directory.
   * @param path - Relative path to the file in the app's data directory
   * @returns Promise resolving to the file contents as string
   * @throws Error if file cannot be read or doesn't exist
   */
  async readFile(path: string): Promise<string> {
    const file = await Filesystem.readFile({
      path,
      directory: Directory.Data,
    });
    if (file.data instanceof Blob) {
      return await file.data.text();
    }
    return file.data;
  }

  /**
   * Writes content to a file in the app's safe storage and offers sharing.
   *
   * Platform-specific behavior:
   * - Saves to app's Documents directory
   * - Offers sharing functionality to move file elsewhere
   *
   * The method handles:
   * 1. Writing to app-safe storage
   * 2. Sharing the file with user's preferred app
   * 3. Error handling and logging
   *
   * @param fileName - The name of the file to create (e.g. "backup.json")
   * @param content - The content to write to the file
   *
   * @throws Error if:
   * - File writing fails
   * - Sharing fails
   *
   * @example
   * ```typescript
   * // Save and share a JSON file
   * await platformService.writeFile(
   *   "backup.json",
   *   JSON.stringify(data)
   * );
   * ```
   */
  async writeFile(fileName: string, content: string): Promise<void> {
    try {
      // Check storage permissions before proceeding
      await this.checkStoragePermissions();

      const logData = {
        targetFileName: fileName,
        contentLength: content.length,
        platform: this.getCapabilities().isIOS ? "iOS" : "Android",
        timestamp: new Date().toISOString(),
      };
      logger.log(
        "Starting writeFile operation",
        JSON.stringify(logData, null, 2),
      );

      // For Android, we need to handle content URIs differently
      if (this.getCapabilities().isIOS) {
        // Write to app's Documents directory for iOS
        const writeResult = await Filesystem.writeFile({
          path: fileName,
          data: content,
          directory: Directory.Data,
          encoding: Encoding.UTF8,
        });

        const writeSuccessLogData = {
          path: writeResult.uri,
          timestamp: new Date().toISOString(),
        };
        logger.log(
          "File write successful",
          JSON.stringify(writeSuccessLogData, null, 2),
        );

        // Offer to share the file
        try {
          await Share.share({
            title: "TimeSafari Backup",
            text: "Here is your TimeSafari backup file.",
            url: writeResult.uri,
            dialogTitle: "Share your backup",
          });

          logger.log(
            "Share dialog shown",
            JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
          );
        } catch (shareError) {
          // Log share error but don't fail the operation
          logger.error(
            "Share dialog failed",
            JSON.stringify(
              {
                error: shareError,
                timestamp: new Date().toISOString(),
              },
              null,
              2,
            ),
          );
        }
      } else {
        // For Android, first write to app's Documents directory
        const writeResult = await Filesystem.writeFile({
          path: fileName,
          data: content,
          directory: Directory.Data,
          encoding: Encoding.UTF8,
        });

        const writeSuccessLogData = {
          path: writeResult.uri,
          timestamp: new Date().toISOString(),
        };
        logger.log(
          "File write successful to app storage",
          JSON.stringify(writeSuccessLogData, null, 2),
        );

        // Then share the file to let user choose where to save it
        try {
          await Share.share({
            title: "TimeSafari Backup",
            text: "Here is your TimeSafari backup file.",
            url: writeResult.uri,
            dialogTitle: "Save your backup",
          });

          logger.log(
            "Share dialog shown for Android",
            JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
          );
        } catch (shareError) {
          // Log share error but don't fail the operation
          logger.error(
            "Share dialog failed for Android",
            JSON.stringify(
              {
                error: shareError,
                timestamp: new Date().toISOString(),
              },
              null,
              2,
            ),
          );
        }
      }
    } catch (error: unknown) {
      const err = error as Error;
      const finalErrorLogData = {
        error: {
          message: err.message,
          name: err.name,
          stack: err.stack,
        },
        timestamp: new Date().toISOString(),
      };
      logger.error(
        "Error in writeFile operation:",
        JSON.stringify(finalErrorLogData, null, 2),
      );
      throw new Error(`Failed to save file: ${err.message}`);
    }
  }

  /**
   * Writes content to a file in the device's app-private storage.
   * Then shares the file using the system share dialog.
   *
   * Works on both Android and iOS without needing external storage permissions.
   *
   * @param fileName - The name of the file to create (e.g. "backup.json")
   * @param content - The content to write to the file
   */
  async writeAndShareFile(fileName: string, content: string): Promise<void> {
    const timestamp = new Date().toISOString();
    const logData = {
      action: "writeAndShareFile",
      fileName,
      contentLength: content.length,
      timestamp,
    };
    logger.log("[CapacitorPlatformService]", JSON.stringify(logData, null, 2));

    try {
      // Check storage permissions before proceeding
      await this.checkStoragePermissions();

      const { uri } = await Filesystem.writeFile({
        path: fileName,
        data: content,
        directory: Directory.Data,
        encoding: Encoding.UTF8,
        recursive: true,
      });

      logger.log("[CapacitorPlatformService] File write successful:", {
        uri,
        timestamp: new Date().toISOString(),
      });

      await Share.share({
        title: "TimeSafari Backup",
        text: "Here is your backup file.",
        url: uri,
        dialogTitle: "Share your backup file",
      });
    } catch (error) {
      const err = error as Error;
      const errLog = {
        message: err.message,
        stack: err.stack,
        timestamp: new Date().toISOString(),
      };
      logger.error(
        "[CapacitorPlatformService] Error writing or sharing file:",
        JSON.stringify(errLog, null, 2),
      );
      throw new Error(`Failed to write or share file: ${err.message}`);
    }
  }

  /**
   * Deletes a file from the app's data directory.
   * @param path - Relative path to the file to delete
   * @throws Error if deletion fails or file doesn't exist
   */
  async deleteFile(path: string): Promise<void> {
    await Filesystem.deleteFile({
      path,
      directory: Directory.Data,
    });
  }

  /**
   * Lists files in the specified directory within app's data directory.
   * @param directory - Relative path to the directory to list
   * @returns Promise resolving to array of filenames
   * @throws Error if directory cannot be read or doesn't exist
   */
  async listFiles(directory: string): Promise<string[]> {
    const result = await Filesystem.readdir({
      path: directory,
      directory: Directory.Data,
    });
    return result.files.map((file) =>
      typeof file === "string" ? file : file.name,
    );
  }

  /**
   * Opens the device camera to take a picture.
   * Configures camera for high quality images with editing enabled.
   * @returns Promise resolving to the captured image data
   * @throws Error if camera access fails or user cancels
   */
  async takePicture(): Promise<ImageResult> {
    try {
      const image = await Camera.getPhoto({
        quality: 90,
        allowEditing: true,
        resultType: CameraResultType.Base64,
        source: CameraSource.Camera,
      });

      const blob = await this.processImageData(image.base64String);
      return {
        blob,
        fileName: `photo_${Date.now()}.${image.format || "jpg"}`,
      };
    } catch (error) {
      logger.error("Error taking picture with Capacitor:", error);
      throw new Error("Failed to take picture");
    }
  }

  /**
   * Opens the device photo gallery to pick an existing image.
   * Configures picker for high quality images with editing enabled.
   * @returns Promise resolving to the selected image data
   * @throws Error if gallery access fails or user cancels
   */
  async pickImage(): Promise<ImageResult> {
    try {
      const image = await Camera.getPhoto({
        quality: 90,
        allowEditing: true,
        resultType: CameraResultType.Base64,
        source: CameraSource.Photos,
      });

      const blob = await this.processImageData(image.base64String);
      return {
        blob,
        fileName: `photo_${Date.now()}.${image.format || "jpg"}`,
      };
    } catch (error) {
      logger.error("Error picking image with Capacitor:", error);
      throw new Error("Failed to pick image");
    }
  }

  /**
   * Converts base64 image data to a Blob.
   * @param base64String - Base64 encoded image data
   * @returns Promise resolving to image Blob
   * @throws Error if conversion fails
   */
  private async processImageData(base64String?: string): Promise<Blob> {
    if (!base64String) {
      throw new Error("No image data received");
    }

    // Convert base64 to blob
    const byteCharacters = atob(base64String);
    const byteArrays = [];
    for (let offset = 0; offset < byteCharacters.length; offset += 512) {
      const slice = byteCharacters.slice(offset, offset + 512);
      const byteNumbers = new Array(slice.length);
      for (let i = 0; i < slice.length; i++) {
        byteNumbers[i] = slice.charCodeAt(i);
      }
      const byteArray = new Uint8Array(byteNumbers);
      byteArrays.push(byteArray);
    }
    return new Blob(byteArrays, { type: "image/jpeg" });
  }

  /**
   * Handles deep link URLs for the application.
   * Note: Capacitor handles deep links automatically.
   * @param _url - The deep link URL (unused)
   */
  async handleDeepLink(_url: string): Promise<void> {
    // Capacitor handles deep links automatically
    // This is just a placeholder for the interface
    return Promise.resolve();
  }

  /**
   * @see PlatformService.dbQuery
   */
  async dbQuery(sql: string, params?: unknown[]): Promise<QueryExecResult> {
    await this.waitForInitialization();
    return this.queueOperation<QueryExecResult>("query", sql, params || []);
  }

  /**
   * @see PlatformService.dbExec
   */
  async dbExec(
    sql: string,
    params?: unknown[],
  ): Promise<{ changes: number; lastId?: number }> {
    await this.waitForInitialization();
    return this.queueOperation<{ changes: number; lastId?: number }>(
      "run",
      sql,
      params || [],
    );
  }
}