/**
 * This file is the SQL replacement of the index.ts file in the db directory.
 * That file will eventually be deleted.
 */

import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { MASTER_SETTINGS_KEY, Settings } from "./tables/settings";
import { logger } from "@/utils/logger";
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
import { QueryExecResult } from "@/interfaces/database";

export async function insertDidSpecificSettings(
  did: string,
  settings: Partial<Settings> = {},
): Promise<boolean> {
  const platform = PlatformServiceFactory.getInstance();
  const { sql, params } = generateInsertStatement(
    { ...settings, accountDid: did }, // make sure accountDid is set to the given value
    "settings",
  );
  const result = await platform.dbExec(sql, params);
  return result.changes === 1;
}

export async function updateDidSpecificSettings(
  accountDid: string,
  settingsChanges: Settings,
): Promise<boolean> {
  settingsChanges.accountDid = accountDid;
  delete settingsChanges.id; // key off account, not ID

  const platform = PlatformServiceFactory.getInstance();

  // First, let's see what's currently in the database
  const checkResult = await platform.dbQuery(
    "SELECT * FROM settings WHERE accountDid = ?",
    [accountDid],
  );

  // Get the current values for comparison
  const currentRecord = checkResult?.values?.length
    ? mapColumnsToValues(checkResult.columns, checkResult.values)[0]
    : null;

  // First try to update existing record
  const { sql: updateSql, params: updateParams } = generateUpdateStatement(
    settingsChanges,
    "settings",
    "accountDid = ?",
    [accountDid],
  );

  await platform.dbExec(updateSql, updateParams);

  // **WORKAROUND**: AbsurdSQL doesn't return changes count correctly
  // Instead, check if the record was actually updated
  const postUpdateResult = await platform.dbQuery(
    "SELECT * FROM settings WHERE accountDid = ?",
    [accountDid],
  );

  const updatedRecord = postUpdateResult?.values?.length
    ? mapColumnsToValues(postUpdateResult.columns, postUpdateResult.values)[0]
    : null;

  // Note that we want to eliminate this check (and fix the above if it doesn't work).
  // Check if any of the target fields were actually changed
  let actuallyUpdated = false;
  if (currentRecord && updatedRecord) {
    for (const key of Object.keys(settingsChanges)) {
      if (key !== "accountDid" && currentRecord[key] !== updatedRecord[key]) {
        actuallyUpdated = true;
      }
    }
  }

  // If the standard update didn't work, try a different approach
  if (
    !actuallyUpdated &&
    settingsChanges.firstName &&
    settingsChanges.isRegistered !== undefined
  ) {
    // Update firstName
    await platform.dbExec(
      "UPDATE settings SET firstName = ? WHERE accountDid = ?",
      [settingsChanges.firstName, accountDid],
    );

    // Update isRegistered
    await platform.dbExec(
      "UPDATE settings SET isRegistered = ? WHERE accountDid = ?",
      [settingsChanges.isRegistered ? 1 : 0, accountDid],
    );

    // Check if the individual updates worked
    const finalCheckResult = await platform.dbQuery(
      "SELECT * FROM settings WHERE accountDid = ?",
      [accountDid],
    );

    const finalRecord = finalCheckResult?.values?.length
      ? mapColumnsToValues(finalCheckResult.columns, finalCheckResult.values)[0]
      : null;

    if (finalRecord) {
      actuallyUpdated =
        finalRecord.firstName === settingsChanges.firstName &&
        finalRecord.isRegistered === (settingsChanges.isRegistered ? 1 : 0);
    }
  }

  return actuallyUpdated;
}

const DEFAULT_SETTINGS: Settings = {
  id: MASTER_SETTINGS_KEY,
  activeDid: undefined,
  apiServer: DEFAULT_ENDORSER_API_SERVER,
};

// retrieves default settings
export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
  const platform = PlatformServiceFactory.getInstance();
  const sql = "SELECT * FROM settings WHERE id = ?";
  const result = await platform.dbQuery(sql, [MASTER_SETTINGS_KEY]);
  if (!result) {
    return DEFAULT_SETTINGS;
  } else {
    const settings = mapColumnsToValues(
      result.columns,
      result.values,
    )[0] as Settings;
    settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
    settings.starredPlanHandleIds = parseJsonField(
      settings.starredPlanHandleIds,
      [],
    );
    return settings;
  }
}

/**
 * Retrieves settings for the active account, merging with default settings
 *
 * @returns Promise<Settings> Combined settings with account-specific overrides
 * @throws Will log specific errors for debugging but returns default settings on failure
 */
export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
  try {
    // Get default settings first
    const defaultSettings = await retrieveSettingsForDefaultAccount();

    // If no active DID, return defaults
    if (!defaultSettings.activeDid) {
      return defaultSettings;
    }

    // Get account-specific settings
    try {
      const platform = PlatformServiceFactory.getInstance();
      const result = await platform.dbQuery(
        "SELECT * FROM settings WHERE accountDid = ?",
        [defaultSettings.activeDid],
      );

      if (!result?.values?.length) {
        // we created DID-specific settings when generated or imported, so this shouldn't happen
        return defaultSettings;
      }

      // Map and filter settings
      const overrideSettings = mapColumnsToValues(
        result.columns,
        result.values,
      )[0] as Settings;

      const overrideSettingsFiltered = Object.fromEntries(
        Object.entries(overrideSettings).filter(([_, v]) => v !== null),
      );

      // Merge settings
      let settings = { ...defaultSettings, ...overrideSettingsFiltered };

      // **ELECTRON-SPECIFIC FIX**: Force production API endpoints for Electron
      // This ensures Electron doesn't use localhost development servers that might be saved in user settings
      if (process.env.VITE_PLATFORM === "electron") {
        // Import constants dynamically to get platform-specific values
        const { DEFAULT_ENDORSER_API_SERVER } = await import(
          "../constants/app"
        );

        settings = {
          ...settings,
          apiServer: DEFAULT_ENDORSER_API_SERVER,
          // Note: partnerApiServer and imageServer are handled by constants/app.ts
        };

        logger.debug(
          `[Electron Settings] Forced API server to: ${DEFAULT_ENDORSER_API_SERVER}`,
        );
      }

      settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
      settings.starredPlanHandleIds = parseJsonField(
        settings.starredPlanHandleIds,
        [],
      );

      return settings;
    } catch (error) {
      logConsoleAndDb(
        `[databaseUtil] Failed to retrieve account settings for ${defaultSettings.activeDid}: ${error}`,
        true,
      );
      // Return defaults on error
      return defaultSettings;
    }
  } catch (error) {
    logConsoleAndDb(
      `[databaseUtil] Failed to retrieve default settings: ${error}`,
      true,
    );
    // Return minimal default settings on complete failure
    return {
      id: MASTER_SETTINGS_KEY,
      activeDid: undefined,
      apiServer: DEFAULT_ENDORSER_API_SERVER,
    };
  }
}

let lastCleanupDate: string | null = null;
export let memoryLogs: string[] = [];

// Flag to prevent infinite logging loops during database operations
let isLoggingToDatabase = false;

/**
 * Logs a message to the database with proper handling of concurrent writes
 * @param message - The message to log
 * @param level - The log level (error, warn, info, debug)
 * @author Matthew Raymer
 */
export async function logToDb(
  message: string,
  level: string = "info",
): Promise<void> {
  // Prevent infinite logging loops - if we're already trying to log to database,
  // just log to console instead to break circular dependency
  if (isLoggingToDatabase) {
    // eslint-disable-next-line no-console
    console.log(`[DB-PREVENTED-${level.toUpperCase()}] ${message}`);
    return;
  }

  // Set flag to prevent circular logging
  isLoggingToDatabase = true;

  try {
    const platform = PlatformServiceFactory.getInstance();
    const todayKey = new Date().toDateString();

    try {
      memoryLogs.push(`${new Date().toISOString()} ${message}`);
      // Insert using actual schema: date, message (no level column)
      await platform.dbExec("INSERT INTO logs (date, message) VALUES (?, ?)", [
        todayKey, // Use date string to match schema
        `[${level.toUpperCase()}] ${message}`, // Include level in message
      ]);

      // Clean up old logs (keep only last 7 days) - do this less frequently
      // Only clean up if the date is different from the last cleanup
      if (!lastCleanupDate || lastCleanupDate !== todayKey) {
        const sevenDaysAgo = new Date(
          new Date().getTime() - 7 * 24 * 60 * 60 * 1000,
        ).toDateString(); // Use date string to match schema
        memoryLogs = memoryLogs.filter(
          (log) => log.split(" ")[0] > sevenDaysAgo,
        );
        await platform.dbExec("DELETE FROM logs WHERE date < ?", [
          sevenDaysAgo,
        ]);
        lastCleanupDate = todayKey;
      }
    } catch (error) {
      // eslint-disable-next-line no-console
      console.error(
        "Error logging to database:",
        error,
        " ... for original message:",
        message,
      );
    }
  } finally {
    // Always reset the flag to prevent permanent blocking of database logging
    isLoggingToDatabase = false;
  }
}

// similar method is in the sw_scripts/additional-scripts.js file
export async function logConsoleAndDb(
  message: string,
  isError = false,
): Promise<void> {
  if (isError) {
    logger.error(message);
  } else {
    logger.log(message);
  }

  await logToDb(message, isError ? "error" : "info");
}

/**
 * Generates SQL INSERT statement and parameters from a model object
 *
 * This helper function creates a parameterized SQL INSERT statement
 * from a JavaScript object. It filters out undefined values and
 * creates the appropriate SQL syntax with placeholders.
 *
 * The function is used internally by the migration functions to
 * safely insert data into the SQLite database.
 *
 * @function generateInsertStatement
 * @param {Record<string, unknown>} model - The model object containing fields to insert
 * @param {string} tableName - The name of the table to insert into
 * @returns {Object} Object containing the SQL statement and parameters array
 * @returns {string} returns.sql - The SQL INSERT statement
 * @returns {unknown[]} returns.params - Array of parameter values
 * @example
 * ```typescript
 * const contact = { did: 'did:example:123', name: 'John Doe' };
 * const { sql, params } = generateInsertStatement(contact, 'contacts');
 * // sql: "INSERT INTO contacts (did, name) VALUES (?, ?)"
 * // params: ['did:example:123', 'John Doe']
 * ```
 */
export function generateInsertStatement(
  model: Record<string, unknown>,
  tableName: string,
): { sql: string; params: unknown[] } {
  const columns = Object.keys(model).filter((key) => model[key] !== undefined);
  const values = Object.values(model)
    .filter((value) => value !== undefined)
    .map((value) => {
      // Convert values to SQLite-compatible types
      if (value === null || value === undefined) {
        return null;
      }
      if (typeof value === "object" && value !== null) {
        // Convert objects and arrays to JSON strings
        return JSON.stringify(value);
      }
      if (typeof value === "boolean") {
        // Convert boolean to integer (0 or 1)
        return value ? 1 : 0;
      }
      // Numbers, strings, bigints, and buffers are already supported
      return value;
    });
  const placeholders = values.map(() => "?").join(", ");
  const insertSql = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`;

  return {
    sql: insertSql,
    params: values,
  };
}

/**
 * Generates SQL UPDATE statement and parameters from a model object
 *
 * This helper function creates a parameterized SQL UPDATE statement
 * from a JavaScript object. It filters out undefined values and
 * creates the appropriate SQL syntax with placeholders.
 *
 * The function is used internally by the migration functions to
 * safely update data in the SQLite database.
 *
 * @function generateUpdateStatement
 * @param {Record<string, unknown>} model - The model object containing fields to update
 * @param {string} tableName - The name of the table to update
 * @param {string} whereClause - The WHERE clause for the update (e.g. "id = ?")
 * @param {unknown[]} [whereParams=[]] - Parameters for the WHERE clause
 * @returns {Object} Object containing the SQL statement and parameters array
 * @returns {string} returns.sql - The SQL UPDATE statement
 * @returns {unknown[]} returns.params - Array of parameter values
 * @example
 * ```typescript
 * const contact = { name: 'Jane Doe' };
 * const { sql, params } = generateUpdateStatement(contact, 'contacts', 'did = ?', ['did:example:123']);
 * // sql: "UPDATE contacts SET name = ? WHERE did = ?"
 * // params: ['Jane Doe', 'did:example:123']
 * ```
 */
export function generateUpdateStatement(
  model: Record<string, unknown>,
  tableName: string,
  whereClause: string,
  whereParams: unknown[] = [],
): { sql: string; params: unknown[] } {
  // Filter out undefined/null values and create SET clause
  const setClauses: string[] = [];
  const params: unknown[] = [];

  Object.entries(model).forEach(([key, value]) => {
    setClauses.push(`${key} = ?`);
    // Convert values to SQLite-compatible types
    let convertedValue = value ?? null;
    if (convertedValue !== null) {
      if (typeof convertedValue === "object") {
        // Convert objects and arrays to JSON strings
        convertedValue = JSON.stringify(convertedValue);
      } else if (typeof convertedValue === "boolean") {
        // Convert boolean to integer (0 or 1)
        convertedValue = convertedValue ? 1 : 0;
      }
    }
    params.push(convertedValue);
  });

  if (setClauses.length === 0) {
    throw new Error("No valid fields to update");
  }

  const sql = `UPDATE ${tableName} SET ${setClauses.join(", ")} WHERE ${whereClause}`;

  return {
    sql,
    params: [...params, ...whereParams],
  };
}

export function mapQueryResultToValues(
  record: QueryExecResult | undefined,
): Array<Record<string, unknown>> {
  if (!record) {
    return [];
  }
  return mapColumnsToValues(record.columns, record.values) as Array<
    Record<string, unknown>
  >;
}

/**
 * Maps an array of column names to an array of value arrays, creating objects where each column name
 * is mapped to its corresponding value.
 * @param columns Array of column names to use as object keys
 * @param values Array of value arrays, where each inner array corresponds to one row of data
 * @returns Array of objects where each object maps column names to their corresponding values
 */
export function mapColumnsToValues(
  columns: string[],
  values: unknown[][],
): Array<Record<string, unknown>> {
  return values.map((row) => {
    const obj: Record<string, unknown> = {};
    columns.forEach((column, index) => {
      obj[column] = row[index];
    });
    return obj;
  });
}

/**
 * Debug function to inspect raw settings data in the database
 * This helps diagnose issues with data corruption or malformed JSON
 * @param did Optional DID to inspect specific account settings
 * @author Matthew Raymer
 */
export async function debugSettingsData(did?: string): Promise<void> {
  try {
    const platform = PlatformServiceFactory.getInstance();

    // Get all settings records
    const allSettings = await platform.dbQuery("SELECT * FROM settings");

    logConsoleAndDb(
      `[DEBUG] Total settings records: ${allSettings?.values?.length || 0}`,
      false,
    );

    if (allSettings?.values?.length) {
      allSettings.values.forEach((row, index) => {
        const settings = mapColumnsToValues(allSettings.columns, [row])[0];
        logConsoleAndDb(`[DEBUG] Settings record ${index + 1}:`, false);
        logConsoleAndDb(`[DEBUG] - ID: ${settings.id}`, false);
        logConsoleAndDb(`[DEBUG] - accountDid: ${settings.accountDid}`, false);
        logConsoleAndDb(`[DEBUG] - activeDid: ${settings.activeDid}`, false);

        if (settings.searchBoxes) {
          logConsoleAndDb(
            `[DEBUG] - searchBoxes type: ${typeof settings.searchBoxes}`,
            false,
          );
          logConsoleAndDb(
            `[DEBUG] - searchBoxes value: ${String(settings.searchBoxes)}`,
            false,
          );

          // Try to parse it
          try {
            const parsed = JSON.parse(String(settings.searchBoxes));
            logConsoleAndDb(
              `[DEBUG] - searchBoxes parsed successfully: ${JSON.stringify(parsed)}`,
              false,
            );
          } catch (parseError) {
            logConsoleAndDb(
              `[DEBUG] - searchBoxes parse error: ${parseError}`,
              true,
            );
          }
        }

        logConsoleAndDb(
          `[DEBUG] - Full record: ${JSON.stringify(settings, null, 2)}`,
          false,
        );
      });
    }

    // If specific DID provided, also check accounts table
    if (did) {
      const account = await platform.dbQuery(
        "SELECT * FROM accounts WHERE did = ?",
        [did],
      );
      logConsoleAndDb(
        `[DEBUG] Account for ${did}: ${JSON.stringify(account, null, 2)}`,
        false,
      );
    }
  } catch (error) {
    logConsoleAndDb(`[DEBUG] Error inspecting settings data: ${error}`, true);
  }
}

/**
 * Platform-agnostic JSON parsing utility
 * Handles different SQLite implementations:
 * - Web SQLite (wa-sqlite/absurd-sql): Auto-parses JSON strings to objects
 * - Capacitor SQLite: Returns raw strings that need manual parsing
 *
 * Maybe consolidate with PlatformServiceMixin._parseJsonField
 *
 * @param value The value to parse (could be string or already parsed object)
 * @param defaultValue Default value if parsing fails
 * @returns Parsed object or default value
 * @author Matthew Raymer
 */
export function parseJsonField<T>(value: unknown, defaultValue: T): T {
  try {
    // If already an object (web SQLite auto-parsed), return as-is
    if (typeof value === "object" && value !== null) {
      return value as T;
    }

    // If it's a string (Capacitor SQLite or fallback), parse it
    if (typeof value === "string") {
      return JSON.parse(value) as T;
    }

    // If it's null/undefined, return default
    if (value === null || value === undefined) {
      return defaultValue;
    }

    return defaultValue;
  } catch (error) {
    logConsoleAndDb(
      `[databaseUtil] Failed to parse JSON field: ${error}`,
      true,
    );
    return defaultValue;
  }
}
