/**
 * 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 updateDefaultSettings(
  settingsChanges: Settings,
): Promise<boolean> {
  delete settingsChanges.accountDid; // just in case
  // ensure there is no "id" that would override the key
  delete settingsChanges.id;
  try {
    const platformService = PlatformServiceFactory.getInstance();
    const { sql, params } = generateUpdateStatement(
      settingsChanges,
      "settings",
      "id = ?",
      [MASTER_SETTINGS_KEY],
    );
    const result = await platformService.dbExec(sql, params);
    return result.changes === 1;
  } catch (error) {
    logger.error("Error updating default settings:", error);
    if (error instanceof Error) {
      throw error; // Re-throw if it's already an Error with a message
    } else {
      throw new Error(
        `Failed to update settings. We recommend you try again or restart the app.`,
      );
    }
  }
}

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 try to update existing record
  const { sql: updateSql, params: updateParams } = generateUpdateStatement(
    settingsChanges,
    "settings",
    "accountDid = ?",
    [accountDid],
  );

  const updateResult = await platform.dbExec(updateSql, updateParams);
  return updateResult.changes === 1;
}

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;
    if (settings.searchBoxes) {
      // @ts-expect-error - the searchBoxes field is a string in the DB
      settings.searchBoxes = JSON.parse(settings.searchBoxes);
    }
    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
      const settings = { ...defaultSettings, ...overrideSettingsFiltered };

      // Handle searchBoxes parsing
      if (settings.searchBoxes) {
        settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
      }

      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[] = [];

/**
 * Logs a message to the database with proper handling of concurrent writes
 * @param message - The message to log
 * @author Matthew Raymer
 */
export async function logToDb(message: string): Promise<void> {
  const platform = PlatformServiceFactory.getInstance();
  const todayKey = new Date().toDateString();
  const nowKey = new Date().toISOString();

  try {
    memoryLogs.push(`${new Date().toISOString()} ${message}`);
    // Try to insert first, if it fails due to UNIQUE constraint, update instead
    await platform.dbExec("INSERT INTO logs (date, message) VALUES (?, ?)", [
      nowKey,
      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,
      );
      memoryLogs = memoryLogs.filter(
        (log) => log.split(" ")[0] > sevenDaysAgo.toDateString(),
      );
      await platform.dbExec("DELETE FROM logs WHERE date < ?", [
        sevenDaysAgo.toDateString(),
      ]);
      lastCleanupDate = todayKey;
    }
  } catch (error) {
    // Log to console as fallback
    // eslint-disable-next-line no-console
    console.error(
      "Error logging to database:",
      error,
      " ... for original message:",
      message,
    );
  }
}

// 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(`${new Date().toISOString()}`, message);
  } else {
    logger.log(`${new Date().toISOString()}`, message);
  }
  await logToDb(message);
}

/**
 * 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);
  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} = ?`);
    params.push(value ?? null);
  });

  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
 *
 * @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;
  }
}
