type AnyMap = Record<string, any>;
type StringMap = Record<string, string>;

// Generic "convert" via JSON
function convert<T>(input: any): T {
  return JSON.parse(JSON.stringify(input)) as T;
}

// Flatten nested objects to map<string, any>
function flattenMap(input: AnyMap): AnyMap {
  const result: AnyMap = {};

  for (const [key, value] of Object.entries(input)) {
    const newKey = toSnakeCase(key);

    if (value === null || value === undefined) continue;

    if (typeof value === 'string') {
      if (value !== '') result[newKey] = value;
    } else if (['number', 'boolean'].includes(typeof value)) {
      result[newKey] = value;
    } else if (Array.isArray(value)) {
      const joined = value
        .filter((v) => v !== null && v !== undefined)
        .join(', ');
      if (joined !== '') result[newKey] = joined;
    } else if (typeof value === 'object') {
      const nested = flattenMap(value as AnyMap);
      Object.assign(result, nested);
    } else {
      result[newKey] = String(value);
    }
  }

  return result;
}

// Flatten nested objects to map<string, string>
function flattenMapToString(input: AnyMap): StringMap {
  const result: StringMap = {};

  for (const [key, value] of Object.entries(input)) {
    const newKey = toSnakeCase(key);

    if (value === null || value === undefined) continue;

    if (typeof value === 'string') {
      if (value !== '') result[newKey] = value;
    } else if (['number', 'boolean'].includes(typeof value)) {
      const str = String(value);
      if (str !== '') result[newKey] = str;
    } else if (Array.isArray(value)) {
      const joined = value
        .filter((v) => v !== null && v !== undefined)
        .map((v) => String(v))
        .join(', ');
      if (joined !== '') result[newKey] = joined;
    } else if (typeof value === 'object') {
      const nested = flattenMapToString(value as AnyMap);
      Object.assign(result, nested);
    } else {
      const str = String(value);
      if (str !== '') result[newKey] = str;
    }
  }

  return result;
}

// Public API equivalents

export const serializeToFlatMapString = (obj: any): StringMap => {
  const initialMap = obj ? convert<AnyMap>(obj) : {};
  return flattenMapToString(initialMap);
};

export const serializeToFlatPrimitiveMap = (obj: any): AnyMap => {
  const initialMap = obj ? convert<AnyMap>(obj) : {};
  return flattenMap(initialMap);
};

export const toSnakeCase = (str: string): string => {
  return str.trim().replace(/\s+/g, '_').toLowerCase();
};
