import { type AsyncIterableX, from } from 'ix/asynciterable';
import { empty } from 'ix/asynciterable/empty';
import { merge } from 'ix/asynciterable/merge';
import { catchError, concatMap, withAbort } from 'ix/asynciterable/operators';

import {
  type DataSourceRuleGroupIdentifier,
  type DataSourceRulesSourceIdentifier,
  type GrafanaRuleGroupIdentifier,
} from 'app/types/unified-alerting';
import {
  type GrafanaPromRuleDTO,
  type GrafanaPromRuleGroupDTO,
  type PromRuleDTO,
  type PromRuleGroupDTO,
} from 'app/types/unified-alerting-dto';

import { RuleSource, type RulesFilter } from '../../search/rulesSearchParser';
import { getDatasourceAPIUid, getExternalRulesSources } from '../../utils/datasource';
import { type RulePositionHash, createRulePositionHash } from '../rulePositionHash';

import { getDatasourceFilter } from './datasourceFilter';
import { getGrafanaFilter } from './grafanaFilter';
import {
  type FetchGroupsLimitOptions,
  useGrafanaGroupsGenerator,
  usePrometheusGroupsGenerator,
} from './prometheusGroupsGenerator';

export type RuleWithOrigin = PromRuleWithOrigin | GrafanaRuleWithOrigin;

export interface GrafanaRuleWithOrigin {
  rule: GrafanaPromRuleDTO;
  groupIdentifier: GrafanaRuleGroupIdentifier;
  /**
   * The name of the namespace that contains the rule group
   * groupIdentifier contains the uid of the namespace, but not the user-friendly display name
   */
  namespaceName: string;
  origin: 'grafana';
}

export interface PromRuleWithOrigin {
  rule: PromRuleDTO;
  groupIdentifier: DataSourceRuleGroupIdentifier;
  origin: 'datasource';
  /**
   * Position hash encoding both the rule's index and the total number of rules in the group.
   * Format: "<index>:<totalRules>" (e.g., "0:3", "1:5")
   *
   * This is used as a tiebreaker when multiple identical rules exist in a group and need to be
   * matched against their counterparts in another rule source (e.g., matching Prometheus rules
   * to Ruler API rules). The hash ensures that identical rules are matched by their position
   * only when both groups have the same structure (same number of rules).
   *
   * @example
   * // Two identical alerts in different positions
   * Rule at position 0 in a 3-rule group: rulePositionHash = "0:3"
   * Rule at position 1 in a 3-rule group: rulePositionHash = "1:3"
   * // These won't match rules in a 2-rule group (e.g., "0:2") even if identical
   */
  rulePositionHash: RulePositionHash;
}

interface GetIteratorResult {
  iterable: AsyncIterableX<RuleWithOrigin>;
  abortController: AbortController;
}

export function useFilteredRulesIteratorProvider() {
  const prometheusGroupsGenerator = usePrometheusGroupsGenerator();
  const grafanaGroupsGenerator = useGrafanaGroupsGenerator({ limitAlerts: 0 });

  const getFilteredRulesIterable = (filterState: RulesFilter, options: FetchGroupsLimitOptions): GetIteratorResult => {
    /* this is the abort controller that allows us to stop an AsyncIterable */
    const abortController = new AbortController();

    const { backendFilter, frontendFilter, hasInvalidDataSourceNames } = getGrafanaFilter(filterState);

    // Short-circuit: if all provided data source names are invalid, return empty results (no rules can match).
    if (hasInvalidDataSourceNames) {
      return { iterable: empty(), abortController };
    }

    const grafanaRulesGenerator: AsyncIterableX<RuleWithOrigin> = from(
      grafanaGroupsGenerator(options.grafanaManagedLimit, backendFilter)
    ).pipe(
      withAbort(abortController.signal),
      concatMap((groups) =>
        groups
          .filter((group) => frontendFilter.groupMatches(group))
          .flatMap((group) => group.rules.map((rule) => ({ group, rule })))
          .filter(({ rule }) => frontendFilter.ruleMatches(rule))
          .map(({ group, rule }) => mapGrafanaRuleToRuleWithOrigin(group, rule))
      ),
      catchError(() => empty())
    );

    // Determine which data sources to use
    const externalRulesSourcesToFetchFrom = getRulesSourcesFromFilter(filterState);

    if (filterState.ruleSource === RuleSource.Grafana) {
      return { iterable: grafanaRulesGenerator, abortController };
    }

    const { groupMatches, ruleMatches } = getDatasourceFilter(filterState);

    const dataSourceGenerators: Array<AsyncIterableX<RuleWithOrigin>> = externalRulesSourcesToFetchFrom.map(
      (dataSourceIdentifier) => {
        const promGroupsGenerator: AsyncIterableX<RuleWithOrigin> = from(
          prometheusGroupsGenerator(dataSourceIdentifier, options.datasourceManagedLimit.groupLimit)
        ).pipe(
          withAbort(abortController.signal),
          concatMap((groups) =>
            groups
              .filter((group) => groupMatches(group))
              .flatMap((group) => group.rules.map((rule, index) => ({ group, rule, index })))
              .filter(({ rule }) => ruleMatches(rule))
              .map(({ group, rule, index }) => mapRuleToRuleWithOrigin(dataSourceIdentifier, group, rule, index))
          ),
          catchError(() => empty())
        );

        return promGroupsGenerator;
      }
    );

    const iterablesToMerge: Array<AsyncIterableX<RuleWithOrigin>> = [];
    const includeGrafana = filterState.ruleSource !== 'datasource';
    const includeExternal = true;

    if (includeGrafana) {
      iterablesToMerge.push(grafanaRulesGenerator);
    }
    if (includeExternal) {
      iterablesToMerge.push(...dataSourceGenerators);
    }

    const iterable = mergeIterables(iterablesToMerge);

    return { iterable, abortController };
  };

  return getFilteredRulesIterable;
}

function mergeIterables(iterables: Array<AsyncIterableX<RuleWithOrigin>>): AsyncIterableX<RuleWithOrigin> {
  if (iterables.length === 0) {
    return empty();
  }
  const [firstIterable, ...rest] = iterables;
  return merge(firstIterable, ...rest);
}

/**
 * Finds all data sources that the user might want to filter by.
 * Only allows Prometheus and Loki data source types.
 * Returns all external rules sources if no filter is provided.
 */
function getRulesSourcesFromFilter(filter: RulesFilter): DataSourceRulesSourceIdentifier[] {
  const allExternalSources = getExternalRulesSources();

  // If no filter is provided, return all external sources
  if (filter.dataSourceNames.length === 0) {
    return allExternalSources;
  }

  return filter.dataSourceNames.reduce<DataSourceRulesSourceIdentifier[]>((acc, dataSourceName) => {
    // since "getDatasourceAPIUid" can throw we'll omit any non-existing data sources
    try {
      const uid = getDatasourceAPIUid(dataSourceName);

      // Only include data sources that exist in allExternalRulesSources
      const existsInExternalSources = allExternalSources.some((source) => source.uid === uid);
      if (!existsInExternalSources) {
        return acc;
      }

      acc.push({
        name: dataSourceName,
        uid,
        ruleSourceType: 'datasource',
      });
    } catch {}

    return acc;
  }, []);
}

function mapRuleToRuleWithOrigin(
  rulesSource: DataSourceRulesSourceIdentifier,
  group: PromRuleGroupDTO,
  rule: PromRuleDTO,
  ruleIndex: number
): PromRuleWithOrigin {
  return {
    rule,
    groupIdentifier: {
      rulesSource,
      namespace: { name: group.file },
      groupName: group.name,
      groupOrigin: 'datasource',
    },
    origin: 'datasource',
    rulePositionHash: createRulePositionHash(ruleIndex, group.rules.length),
  };
}

function mapGrafanaRuleToRuleWithOrigin(
  group: GrafanaPromRuleGroupDTO,
  rule: GrafanaPromRuleDTO
): GrafanaRuleWithOrigin {
  return {
    rule,
    groupIdentifier: {
      namespace: { uid: group.folderUid },
      groupName: group.name,
      groupOrigin: 'grafana',
    },
    namespaceName: group.file,
    origin: 'grafana',
  };
}
