// Pure transforms between the UI filter state and the backend filter payload,
// kept framework-agnostic so any feature can reuse them.

import { HOST_SEARCH_ATTRIBUTE, INVENTORY_FILTER_OPERATORS, VALUELESS_CONDITIONS } from '../constants';
import type {
  CfeAttribute,
  EntriesMap,
  FilterPayload,
  FilterRule,
  FilterValue,
  GroupFilter,
  HostFilterPayload,
  InventoryFilterState,
  InventoryMapping
} from '../types';

// Client-only stable id for React keys; never reaches the backend.
let uidCounter = 0;
export const nextUid = (): string => `f${++uidCounter}`;

export const emptyEntriesFilter = () => ({ includes: {}, excludes: {} });

export const emptyFilterState = (): InventoryFilterState => ({
  filters: [],
  entriesFilter: emptyEntriesFilter()
});

const stripPrefix = (name: string) => name.replace(/^attribute_name=/, '');

const operatorFor = (filter: FilterRule): string => {
  // slist "="/"!=" collapse onto matches/not_match.
  if (filter.type === 'slist') {
    if (filter.condition === '=') return INVENTORY_FILTER_OPERATORS.matches;
    if (filter.condition === '!=') return INVENTORY_FILTER_OPERATORS.not_match;
  }
  return INVENTORY_FILTER_OPERATORS[filter.condition as string];
};

const processFilterItem = (filter: FilterRule): Record<string, Record<string, FilterValue>> | null => {
  if (filter.type === 'class') return null;

  // Value-less conditions (is reported / is not reported) are presence checks:
  // the UI hides the value input, so the value stays undefined but the rule is
  // still complete. The backend expects the operator with an empty value.
  const isValueless = filter.condition !== undefined && VALUELESS_CONDITIONS.includes(filter.condition);
  if (filter.value === undefined && !isValueless) return null;

  const attributeName = stripPrefix(filter.attribute_name || filter.label || '');
  const operator = operatorFor(filter);
  // An unnamed rule would serialize under an empty key and silently replace the
  // saved filter, so drop it instead.
  if (!attributeName || !operator) return null;

  if (isValueless) return { [attributeName]: { [operator]: '' } };

  // slist replaces the FIRST comma with `%`.
  const value = filter.type === 'slist' ? String(filter.value).replace(',', '%') : filter.value;

  return { [attributeName]: { [operator]: value as FilterValue } };
};

// Merge a processed item into the accumulator. Same attribute+operator used
// twice collapses to an array value.
const combineFilters = (acc: Record<string, Record<string, FilterValue>>, item: Record<string, Record<string, FilterValue>> | null) => {
  if (!item) return;
  const key = Object.keys(item)[0];
  if (Object.prototype.hasOwnProperty.call(acc, key)) {
    const condition = Object.keys(item[key])[0];
    if (Object.prototype.hasOwnProperty.call(acc[key], condition)) {
      const existing = acc[key][condition];
      const asArray = Array.isArray(existing) ? existing : [existing];
      asArray.push(item[key][condition] as string | number);
      item[key][condition] = asArray;
    }
    acc[key] = { ...acc[key], ...item[key] };
  } else {
    Object.assign(acc, item);
  }
};

export const processFilters = (filters: FilterRule[]): FilterPayload => {
  if (!filters || filters.length === 0) return null;
  const processed: Record<string, Record<string, FilterValue>> = {};
  filters.forEach(filter => combineFilters(processed, processFilterItem(filter)));
  return Object.keys(processed).length !== 0 ? processed : null;
};

export const processEntriesFilters = (state: InventoryFilterState): HostFilterPayload | null => {
  const { entriesFilter } = state;
  const includeAdditionally = Array.isArray(state.filters) && state.filters.length > 0;
  const result: HostFilterPayload = {};

  if (entriesFilter && Object.keys(entriesFilter.includes).length > 0) {
    result.includes = { includeAdditionally, entries: entriesFilter.includes };
  }
  if (entriesFilter && Object.keys(entriesFilter.excludes).length > 0) {
    result.excludes = { entries: entriesFilter.excludes };
  }
  return Object.keys(result).length > 0 ? result : null;
};

// Class rules are not part of the attribute filter payload — their values are
// collected into a separate hostContextInclude array.
export const processClassFilters = (filters: FilterRule[]): string[] =>
  (filters || []).filter(filter => filter.type === 'class').map(filter => String(filter.value));

// Assemble the `{ filter, hostFilter, hostContextInclude }` object persisted on
// a group and sent to the inventory report.
export const buildGroupFilter = (state: InventoryFilterState): Pick<GroupFilter, 'filter' | 'hostFilter' | 'hostContextInclude'> => ({
  filter: processFilters(state.filters),
  hostFilter: processEntriesFilters(state),
  hostContextInclude: processClassFilters(state.filters)
});

// Build the virtual host-search filter row for a free-text query.
export const filterFromSearchQuery = (searchQuery: string): FilterRule => ({
  uid: nextUid(),
  id: `attribute_name=attribute_name=${HOST_SEARCH_ATTRIBUTE}`,
  attribute_name: `attribute_name=${HOST_SEARCH_ATTRIBUTE}`,
  category: 'search',
  readonly: 1,
  type: 'string',
  convert_function: null,
  keyname: null,
  enabled: 1,
  label: 'Host search',
  value: searchQuery,
  condition: 'matches'
});

export const isAttributeSearchQuery = (attributeName: string) => attributeName.includes(HOST_SEARCH_ATTRIBUTE);

// Rebuild the UI filter rows from a saved group's `filter` object (reverse of
// processFilters). Array values re-expand into one row per value.
export const convertFilterObjectToFilters = (filtersObject: GroupFilter | undefined, mapping: InventoryMapping): FilterRule[] => {
  const filters: FilterRule[] = [];
  if (!filtersObject || Object.keys(filtersObject).length === 0) return filters;

  if (filtersObject.filter) {
    Object.entries(filtersObject.filter).forEach(([invAttribute, conditions]) => {
      Object.entries(conditions).forEach(([condition, value]) => {
        const isHostSearch = invAttribute === HOST_SEARCH_ATTRIBUTE;
        // An attribute no longer reported by any host has no mapping entry, so
        // it can neither be rendered nor saved back; skip it rather than
        // building a nameless rule.
        if (!isHostSearch && !mapping[invAttribute]) return;
        const mapped: Partial<CfeAttribute> | FilterRule = isHostSearch ? filterFromSearchQuery(String(value)) : mapping[invAttribute];
        const base = { ...mapped, condition };
        const values = Array.isArray(value) ? value : [value];
        values.forEach(v => filters.push({ ...base, uid: nextUid(), value: v as string | number }));
      });
    });
  }

  if (filtersObject.hostContextInclude) {
    filtersObject.hostContextInclude.forEach(value => filters.push({ uid: nextUid(), type: 'class', value, condition: 'matches' }));
  }

  return filters;
};

// Convert the hostkey-only `hostsFilter` into the current entries shape.
export const hostsFilterToEntries = (hostsFilter: GroupFilter['hostsFilter']): { excludes: EntriesMap; includes: EntriesMap } => {
  const includes: EntriesMap = {};
  const excludes: EntriesMap = {};
  if (hostsFilter?.includes) {
    const src = hostsFilter.includes as { data?: string[] };
    includes.hostkey = src.data || Object.keys(hostsFilter.includes);
  }
  if (hostsFilter?.excludes) {
    const src = hostsFilter.excludes as { data?: string[] };
    excludes.hostkey = src.data || Object.keys(hostsFilter.excludes);
  }
  return { includes, excludes };
};

// Rebuild the entries filter from a saved group. A group carries either the
// older hostkey-only `hostsFilter` or the multi-entry-type `hostFilter`; the
// older shape wins when present.
export const entriesFilterFromGroup = (groupFilter: GroupFilter | undefined) => {
  if (!groupFilter) return emptyEntriesFilter();
  if (groupFilter.hostsFilter) {
    return hostsFilterToEntries(groupFilter.hostsFilter);
  }
  return {
    includes: { ...(groupFilter.hostFilter?.includes?.entries || {}) },
    excludes: { ...(groupFilter.hostFilter?.excludes?.entries || {}) }
  };
};
