/**
 * Unified API client for Additional Information endpoints
 * Provides consistent error handling, logging, and CRUD operations
 */
window.AdditionalInformationApi = {
  // Configuration
  _debug: false,

  /**
   * Enable or disable debug logging
   * @param {boolean} enabled - Whether to enable debug mode
   */
  setDebug(enabled) {
    this._debug = enabled;
  },

  /**
   * Internal logging helper
   * @private
   */
  _log(action, data) {
    if (this._debug) {
      console.log(`[AdditionalInformationApi] ${action}:`, data);
    }
  },
  //FIXME: this has the side effect of users not being able to explicitly set an empty string for a field.
  /**
   * Remove empty strings from payload recursively
   * @param {any} value
   * @returns {any}
   */
  sanitizePayload(value) {
    if (Array.isArray(value)) {
      return value
        .map((item) => this.sanitizePayload(item))
        .filter((item) => item !== "");
    }

    if (value && typeof value === "object") {
      const cleaned = {};
      for (const [key, item] of Object.entries(value)) {
        if (item === "") continue;
        cleaned[key] = this.sanitizePayload(item);
      }
      return cleaned;
    }

    return value;
  },

  /**
   * Get CSRF token from meta tag
   * @returns {string} CSRF token
   */
  getCsrfToken() {
    return document.querySelector("[name=csrf-token]")?.content || "";
  },

  /**
   * Build headers for API requests
   * @private
   */
  _buildHeaders(includeContentType = true) {
    const headers = {
      "X-CSRFToken": this.getCsrfToken(),
    };
    if (includeContentType) {
      headers["Content-Type"] = "application/json";
    }
    return headers;
  },

  /**
   * Handle API response with consistent error handling
   * @private
   */
  async _handleResponse(response, action) {
    if (!response.ok) {
      let errorData;
      try {
        errorData = await response.json();
      } catch {
        errorData = { error: response.statusText };
      }

      // Try to parse the error if it's a JSON string
      let parsedError = errorData;
      const errorStr = errorData.detail || errorData.error;
      if (typeof errorStr === "string" && errorStr.startsWith("{")) {
        try {
          parsedError = JSON.parse(errorStr);
        } catch {
          // Not JSON, use as-is
        }
      }

      // If it's a structured validation error, throw with field errors
      if (parsedError.type === "validation_error" && parsedError.field_errors) {
        this._log(`${action} VALIDATION ERROR`, parsedError);
        const error = new Error(parsedError.message || "Validation failed");
        error.fieldErrors = parsedError.field_errors;
        error.isValidationError = true;
        throw error;
      }

      // General error
      const errorMsg =
        parsedError.message ||
        parsedError.error ||
        parsedError.detail ||
        `${action} failed: ${response.statusText}`;
      this._log(`${action} ERROR`, {
        status: response.status,
        error: errorMsg,
      });
      throw new Error(errorMsg);
    }

    const data = await response.json();
    this._log(`${action} SUCCESS`, data);
    return data;
  },

  /**
   * Load all available schemas
   * @returns {Promise<Object>} Object with schema definitions
   */
  async loadSchemas() {
    this._log("loadSchemas", "Starting...");
    const response = await fetch("/api/v1/information/schema/");
    return this._handleResponse(response, "loadSchemas");
  },

  /**
   * Load additional information items for a scenario
   * @param {string} scenarioUuid - UUID of the scenario
   * @returns {Promise<Array>} Array of additional information items
   */
  async loadItems(scenarioUuid) {
    this._log("loadItems", { scenarioUuid });
    const response = await fetch(
      `/api/v1/scenario/${scenarioUuid}/information/`,
    );
    return this._handleResponse(response, "loadItems");
  },

  /**
   * Load both schemas and items in parallel
   * @param {string} scenarioUuid - UUID of the scenario
   * @returns {Promise<{schemas: Object, items: Array}>}
   */
  async loadSchemasAndItems(scenarioUuid) {
    this._log("loadSchemasAndItems", { scenarioUuid });

    const [schemas, items] = await Promise.all([
      this.loadSchemas(),
      this.loadItems(scenarioUuid),
    ]);

    return { schemas, items };
  },

  /**
   * Create new additional information for a scenario
   * @param {string} scenarioUuid - UUID of the scenario
   * @param {string} modelName - Name/type of the additional information model
   * @param {Object} data - Data for the new item
   * @returns {Promise<{status: string, uuid: string}>}
   */
  async createItem(scenarioUuid, modelName, data) {
    const sanitizedData = this.sanitizePayload(data);
    this._log("createItem", { scenarioUuid, modelName, data: sanitizedData });

    // Normalize model name to lowercase
    const normalizedName = modelName.toLowerCase();

    const response = await fetch(
      `/api/v1/scenario/${scenarioUuid}/information/${normalizedName}/`,
      {
        method: "POST",
        headers: this._buildHeaders(),
        body: JSON.stringify(sanitizedData),
      },
    );

    return this._handleResponse(response, "createItem");
  },

    /**
   * Create new additional information and attach it to an object.

   * @param {string} modelName - Name/type of the additional information model
   * @param {Object} data - Data for the new item
   * @param {string} attachObjectUrl - UUID of the object this data should be attached to
   * @param {string} scenarioUuid - UUID of the scenario
   * @returns {Promise<{status: string, uuid: string}>}
   */
  async createItemOnNonScenarioObject(modelName, data, attachObjectUrl, scenarioUuid) {
    const sanitizedData = this.sanitizePayload(data);
    this._log("createItemOnNonScenarioObject", { modelName, data: sanitizedData, attachObjectUrl, scenarioUuid });

    sanitizedData.attach_obj_url = attachObjectUrl;
    if (scenarioUuid) {
      sanitizedData.scenario_uuid = scenarioUuid;
    }


    // Normalize model name to lowercase
    const normalizedName = modelName.toLowerCase();

    const response = await fetch(
      `/api/v1/information/${normalizedName}/`,
      {
        method: "POST",
        headers: this._buildHeaders(),
        body: JSON.stringify(sanitizedData),
      },
    );

    return this._handleResponse(response, "createItemOnNonScenarioObject");
  },

  /**
   * Delete additional information from a scenario
   * @param {string} scenarioUuid - UUID of the scenario
   * @param {string} itemUuid - UUID of the item to delete
   * @returns {Promise<{status: string}>}
   */
  async deleteItem(scenarioUuid, itemUuid) {
    this._log("deleteItem", { scenarioUuid, itemUuid });

    const response = await fetch(
      `/api/v1/scenario/${scenarioUuid}/information/item/${itemUuid}/`,
      {
        method: "DELETE",
        headers: this._buildHeaders(false),
      },
    );

    return this._handleResponse(response, "deleteItem");
  },

  /**
   * Update existing additional information
   * Tries PATCH first, falls back to delete+recreate if not supported
   * @param {string} scenarioUuid - UUID of the scenario
   * @param {Object} item - Item object with uuid, type, and data properties
   * @returns {Promise<{status: string, uuid: string}>}
   */
  async updateItem(scenarioUuid, item) {
    const sanitizedData = this.sanitizePayload(item.data);
    this._log("updateItem", {
      scenarioUuid,
      item: { ...item, data: sanitizedData },
    });

    const { uuid, type } = item;

    // Try PATCH first (preferred method - preserves UUID)
    const response = await fetch(
      `/api/v1/scenario/${scenarioUuid}/information/item/${uuid}/`,
      {
        method: "PATCH",
        headers: this._buildHeaders(),
        body: JSON.stringify(sanitizedData),
      },
    );

    if (response.status === 405) {
      // PATCH not supported, fall back to delete+recreate
      this._log(
        "updateItem",
        "PATCH not supported, falling back to delete+recreate",
      );
      await this.deleteItem(scenarioUuid, uuid);
      return await this.createItem(scenarioUuid, type, sanitizedData);
    }

    return this._handleResponse(response, "updateItem");
  },

  /**
   * Update multiple items sequentially to avoid race conditions
   * @param {string} scenarioUuid - UUID of the scenario
   * @param {Array<Object>} items - Array of items to update
   * @returns {Promise<Array>} Array of results with success status
   */
  async updateItems(scenarioUuid, items) {
    this._log("updateItems", { scenarioUuid, itemCount: items.length });

    const results = [];
    for (const item of items) {
      try {
        const result = await this.updateItem(scenarioUuid, item);
        results.push({
          success: true,
          oldUuid: item.uuid,
          newUuid: result.uuid,
        });
      } catch (error) {
        results.push({
          success: false,
          oldUuid: item.uuid,
          error: error.message,
          fieldErrors: error.fieldErrors,
          isValidationError: error.isValidationError,
        });
      }
    }

    const failed = results.filter((r) => !r.success);
    if (failed.length > 0) {
      // If all failures are validation errors, return all validation errors for display
      const validationErrors = failed.filter((f) => f.isValidationError);
      if (validationErrors.length === failed.length) {
        // All failures are validation errors - return all field errors by item UUID
        const allFieldErrors = {};
        validationErrors.forEach((ve) => {
          allFieldErrors[ve.oldUuid] = ve.fieldErrors || {};
        });
        const error = new Error(
          `${failed.length} item(s) have validation errors. Please correct them.`,
        );
        error.fieldErrors = allFieldErrors; // Map of UUID -> field errors
        error.isValidationError = true;
        error.isMultipleErrors = true; // Flag indicating multiple items have errors
        throw error;
      }
      // Multiple failures or mixed errors - show count
      throw new Error(
        `Failed to update ${failed.length} item(s). Please check the form for errors.`,
      );
    }

    return results;
  },

  /**
   * Create a new scenario with optional additional information
   * @param {string} packageUuid - UUID of the package
   * @param {Object} payload - Scenario data matching ScenarioCreateSchema
   * @param {string} payload.name - Scenario name (required)
   * @param {string} payload.description - Scenario description (optional, default: "")
   * @param {string} payload.scenario_date - Scenario date (optional, default: "No date")
   * @param {string} payload.scenario_type - Scenario type (optional, default: "Not specified")
   * @param {Array} payload.additional_information - Array of additional information (optional, default: [])
   * @returns {Promise<{uuid, url, name, description, review_status, package}>}
   */
  async createScenario(packageUuid, payload) {
    this._log("createScenario", { packageUuid, payload });
    const response = await fetch(`/api/v1/package/${packageUuid}/scenario/`, {
      method: "POST",
      headers: this._buildHeaders(),
      body: JSON.stringify(payload),
    });
    return this._handleResponse(response, "createScenario");
  },

  /**
   * Load all available group names
   * @returns {Promise<{groups: string[]}>}
   */
  async loadGroups() {
    this._log("loadGroups", "Starting...");
    const response = await fetch("/api/v1/information/groups/");
    return this._handleResponse(response, "loadGroups");
  },

  /**
   * Load model definitions for a specific group
   * @param {string} groupName - One of 'soil', 'sludge', 'sediment'
   * @returns {Promise<Object>} Object with subcategories as keys and arrays of model info
   */
  async loadGroupModels(groupName) {
    this._log("loadGroupModels", { groupName });
    const response = await fetch(`/api/v1/information/groups/${groupName}/`);
    return this._handleResponse(response, `loadGroupModels-${groupName}`);
  },

  /**
   * Load model information for multiple groups in parallel
   * @param {Array<string>} groupNames - Defaults to ['soil', 'sludge', 'sediment']
   * @returns {Promise<Object>} Object with group names as keys
   */
  async loadGroupsWithModels(groupNames = ["soil", "sludge", "sediment"]) {
    this._log("loadGroupsWithModels", { groupNames });

    const results = {};
    const promises = groupNames.map(async (groupName) => {
      try {
        results[groupName] = await this.loadGroupModels(groupName);
      } catch (err) {
        this._log(`loadGroupsWithModels-${groupName} ERROR`, err);
        results[groupName] = {};
      }
    });

    await Promise.all(promises);
    return results;
  },

  /**
   * Helper to organize schemas by group based on group model information
   * @param {Object} schemas - Full schema map from loadSchemas()
   * @param {Object} groupModelsData - Group models data from loadGroupsWithModels()
   * @returns {Object} Object with group names as keys and filtered schemas as values
   */
  organizeSchemasByGroup(schemas, groupModelsData) {
    this._log("organizeSchemasByGroup", {
      schemaCount: Object.keys(schemas).length,
      groupCount: Object.keys(groupModelsData).length,
    });

    const organized = {};

    for (const groupName in groupModelsData) {
      organized[groupName] = {};
      const groupData = groupModelsData[groupName];

      // Iterate through subcategories in the group
      for (const subcategory in groupData) {
        for (const model of groupData[subcategory]) {
          // Look up schema by lowercase model name
          if (schemas[model.name]) {
            organized[groupName][model.name] = schemas[model.name];
          }
        }
      }
    }

    return organized;
  },

  /**
   * Convenience method that loads schemas and organizes them by group in one call
   * @param {Array<string>} groupNames - Defaults to ['soil', 'sludge', 'sediment']
   * @returns {Promise<{schemas, groupSchemas, groupModels}>}
   */
  async loadSchemasWithGroups(groupNames = ["soil", "sludge", "sediment"]) {
    this._log("loadSchemasWithGroups", { groupNames });

    // Load schemas and all groups in parallel
    const [schemas, groupModels] = await Promise.all([
      this.loadSchemas(),
      this.loadGroupsWithModels(groupNames),
    ]);

    // Organize schemas by group
    const groupSchemas = this.organizeSchemasByGroup(schemas, groupModels);

    return { schemas, groupSchemas, groupModels };
  },
};
