import { getAllProviderGroups } from "@/actions/manage-groups/manage-groups";
import {
  listOrganizationsSafe,
  listOrganizationUnitsSafe,
} from "@/actions/organizations/organizations";
import { getAllProviders, getProviders } from "@/actions/providers";
import { PROVIDERS_FILTER_PARAM } from "@/actions/providers/providers-filters";
import { getSchedules } from "@/actions/schedules";
import {
  extractFiltersAndQuery,
  extractSortAndKey,
} from "@/lib/helper-filters";
import {
  buildProviderScheduleSummary,
  buildScheduleAttributesFromProvider,
  buildSchedulesByProviderId,
  isScheduleConfigured,
} from "@/lib/schedules";
import {
  FilterEntity,
  FilterOption,
  OrganizationListResponse,
  OrganizationUnitListResponse,
  OrganizationUnitResource,
  ProvidersApiResponse,
  SearchParamsProps,
} from "@/types";
import {
  PROVIDERS_GROUP_KIND,
  PROVIDERS_PAGE_FILTER,
  PROVIDERS_ROW_TYPE,
  ProvidersAccountsViewData,
  ProvidersOrganizationRow,
  ProvidersProviderRow,
  ProvidersTableRow,
  ProvidersTableRowsInput,
} from "@/types/providers-table";
import { ScanScheduleSummary } from "@/types/scans";
import { ScheduleAttributes } from "@/types/schedules";

const PROVIDERS_STATUS_MAPPING = [
  {
    true: {
      label: "Connected",
      value: "true",
    },
  },
  {
    false: {
      label: "Not connected",
      value: "false",
    },
  },
] as Array<{ [key: string]: FilterEntity }>;

interface ProvidersAccountsViewInput {
  isCloud: boolean;
  searchParams: SearchParamsProps;
}

function hasActionError(result: unknown): result is {
  error: unknown;
} {
  return Boolean(
    result &&
      typeof result === "object" &&
      "error" in (result as Record<string, unknown>) &&
      (result as Record<string, unknown>).error !== null &&
      (result as Record<string, unknown>).error !== undefined,
  );
}

async function resolveActionResult<T>(
  action: Promise<T | undefined>,
  fallback?: T,
): Promise<T | undefined> {
  try {
    const result = await action;

    if (hasActionError(result)) {
      return fallback;
    }

    return result ?? fallback;
  } catch {
    return fallback;
  }
}

const createProvidersFilters = (): FilterOption[] => {
  return [
    {
      key: PROVIDERS_PAGE_FILTER.STATUS,
      labelCheckboxGroup: "Status",
      values: ["true", "false"],
      valueLabelMapping: PROVIDERS_STATUS_MAPPING,
      index: 0,
    },
  ];
};

const createProviderGroupLookup = (
  providersResponse?: ProvidersApiResponse,
): Map<string, string> => {
  const lookup = new Map<string, string>();

  for (const includedItem of providersResponse?.included ?? []) {
    if (
      includedItem.type === "provider-groups" &&
      typeof includedItem.attributes?.name === "string"
    ) {
      lookup.set(includedItem.id, includedItem.attributes.name);
    }
  }

  return lookup;
};

// A schedule is backed by the Provider row itself, so its `/schedules` entry
// exists before the first scheduled Scan is materialized — only enabled,
// configured ones carry a displayable cadence summary.
const buildProviderScheduleSummaryFor = (
  attributes: ScheduleAttributes | undefined,
  now: Date,
): ScanScheduleSummary | undefined =>
  attributes && attributes.scan_enabled && isScheduleConfigured(attributes)
    ? buildProviderScheduleSummary(attributes, now)
    : undefined;

const getProviderLastScanAt = (
  provider: ProvidersApiResponse["data"][number],
): string | null => {
  if (
    Object.prototype.hasOwnProperty.call(provider.attributes, "last_scan_at")
  ) {
    return provider.attributes.last_scan_at ?? null;
  }

  return provider.attributes.connection.last_checked_at ?? null;
};

const enrichProviders = (
  providersResponse: ProvidersApiResponse | undefined,
  schedulesByProviderId: Record<string, ScheduleAttributes>,
): ProvidersProviderRow[] => {
  const providerGroupLookup = createProviderGroupLookup(providersResponse);
  const now = new Date();

  return (providersResponse?.data ?? []).map((provider) => {
    const providerScheduleAttributes = buildScheduleAttributesFromProvider(
      provider.attributes,
    );
    const scheduleAttributes =
      providerScheduleAttributes ?? schedulesByProviderId[provider.id];
    const scheduleSummary = buildProviderScheduleSummaryFor(
      scheduleAttributes,
      now,
    );

    return {
      ...provider,
      rowType: PROVIDERS_ROW_TYPE.PROVIDER,
      groupNames:
        provider.relationships.provider_groups.data.map(
          (providerGroup: { id: string }) =>
            providerGroupLookup.get(providerGroup.id) ?? "Unknown Group",
        ) ?? [],
      // Provider scan_* fields are authoritative when present; otherwise we
      // only fall back to the /schedules resource, never materialized scans.
      hasSchedule: scheduleSummary !== undefined,
      scheduleSummary,
      lastScanAt: getProviderLastScanAt(provider),
    };
  });
};

const createOrganizationRow = ({
  groupKind,
  id,
  name,
  externalId,
  organizationId,
  parentExternalId,
  providerIds,
  subRows,
}: {
  externalId: string | null;
  groupKind: ProvidersOrganizationRow["groupKind"];
  id: string;
  name: string;
  organizationId: string | null;
  parentExternalId: string | null;
  providerIds: string[];
  subRows: ProvidersTableRow[];
}): ProvidersOrganizationRow => ({
  id,
  rowType: PROVIDERS_ROW_TYPE.ORGANIZATION,
  groupKind,
  name,
  externalId,
  organizationId,
  parentExternalId,
  providerCount: providerIds.length,
  providerIds,
  subRows,
});

function getRelationshipProviderIds(
  relationships:
    | {
        providers?: {
          data?: Array<{ id: string; type: string }>;
        };
      }
    | undefined,
): string[] {
  return relationships?.providers?.data?.map((provider) => provider.id) ?? [];
}

function getOrganizationUnitParentId(
  organizationUnit: OrganizationUnitResource,
): string | null {
  return organizationUnit.relationships.parent?.data?.id ?? null;
}

function getProviderRowsByIds({
  providerIds,
  providerLookup,
}: {
  providerIds: string[];
  providerLookup: Map<string, ProvidersProviderRow>;
}): ProvidersProviderRow[] {
  return providerIds
    .map((providerId) => providerLookup.get(providerId))
    .filter((provider): provider is ProvidersProviderRow => Boolean(provider));
}

function dedupeIds(ids: string[]): string[] {
  return Array.from(new Set(ids));
}

function collectOrganizationRowProviderIds(
  rows: ProvidersOrganizationRow[],
): string[] {
  return dedupeIds(rows.flatMap((row) => row.providerIds));
}

function getOrganizationUnitRelationshipId(
  provider: ProvidersProviderRow,
): string | null {
  return (
    provider.relationships.organization_unit?.data?.id ??
    provider.relationships.organizational_unit?.data?.id ??
    null
  );
}

function buildOrganizationUnitRows({
  organizationId,
  organizationUnits,
  providerLookup,
  providersByOrganizationUnitId,
  useParentIdRelationships,
  parentExternalId,
  parentOrganizationUnitId,
  maxDepth = 10,
}: {
  organizationId: string;
  organizationUnits: OrganizationUnitResource[];
  parentExternalId: string | null;
  parentOrganizationUnitId: string | null;
  providerLookup: Map<string, ProvidersProviderRow>;
  providersByOrganizationUnitId: Map<string, ProvidersProviderRow[]>;
  useParentIdRelationships: boolean;
  maxDepth?: number;
}): ProvidersOrganizationRow[] {
  if (maxDepth <= 0) {
    return [];
  }

  return organizationUnits
    .filter(
      (organizationUnit) =>
        organizationUnit.relationships.organization.data.id ===
          organizationId &&
        (useParentIdRelationships
          ? getOrganizationUnitParentId(organizationUnit) ===
            parentOrganizationUnitId
          : organizationUnit.attributes.parent_external_id ===
            parentExternalId),
    )
    .map((organizationUnit) => {
      const childOrganizationUnitRows = buildOrganizationUnitRows({
        organizationId,
        organizationUnits,
        parentOrganizationUnitId: organizationUnit.id,
        parentExternalId: organizationUnit.attributes.external_id,
        providerLookup,
        providersByOrganizationUnitId,
        useParentIdRelationships,
        maxDepth: maxDepth - 1,
      });
      const providerRowsFromRelationships = getProviderRowsByIds({
        providerIds: getRelationshipProviderIds(organizationUnit.relationships),
        providerLookup,
      });
      const providerRows =
        providerRowsFromRelationships.length > 0
          ? providerRowsFromRelationships
          : (providersByOrganizationUnitId.get(organizationUnit.id) ?? []);
      const subRows = [...childOrganizationUnitRows, ...providerRows];
      const directProviderIds =
        providerRowsFromRelationships.length > 0
          ? getRelationshipProviderIds(organizationUnit.relationships)
          : providerRows.map((provider) => provider.id);
      const childProviderIds = collectOrganizationRowProviderIds(
        childOrganizationUnitRows,
      );

      return createOrganizationRow({
        groupKind: PROVIDERS_GROUP_KIND.ORGANIZATION_UNIT,
        id: organizationUnit.id,
        name: organizationUnit.attributes.name,
        externalId: organizationUnit.attributes.external_id,
        organizationId,
        parentExternalId: organizationUnit.attributes.parent_external_id,
        providerIds: dedupeIds([...childProviderIds, ...directProviderIds]),
        subRows,
      });
    })
    .filter(
      (organizationUnitRow) => organizationUnitRow.providerIds.length > 0,
    );
}

export function buildProvidersTableRows({
  isCloud,
  organizations,
  organizationUnits,
  providers,
}: ProvidersTableRowsInput): ProvidersTableRow[] {
  if (!isCloud) {
    return providers;
  }

  const providerLookup = new Map(
    providers.map((provider) => [provider.id, provider] as const),
  );
  const providersByOrganizationId = new Map<string, ProvidersProviderRow[]>();
  const providersByOrganizationUnitId = new Map<
    string,
    ProvidersProviderRow[]
  >();

  for (const provider of providers) {
    const organizationId =
      provider.relationships.organization?.data?.id ?? null;
    const organizationUnitId = getOrganizationUnitRelationshipId(provider);

    if (organizationUnitId) {
      const organizationUnitProviders =
        providersByOrganizationUnitId.get(organizationUnitId) ?? [];
      organizationUnitProviders.push(provider);
      providersByOrganizationUnitId.set(
        organizationUnitId,
        organizationUnitProviders,
      );
      continue;
    }

    if (organizationId) {
      const organizationProviders =
        providersByOrganizationId.get(organizationId) ?? [];
      organizationProviders.push(provider);
      providersByOrganizationId.set(organizationId, organizationProviders);
    }
  }

  const useParentIdRelationships = organizationUnits.some(
    (organizationUnit) => organizationUnit.relationships.parent !== undefined,
  );

  // Build a set of provider IDs that are assigned to OUs, so we can
  // exclude them from the org's direct children and avoid duplication.
  const providersAssignedToOu = new Set(
    Array.from(providersByOrganizationUnitId.values()).flatMap((providers) =>
      providers.map((p) => p.id),
    ),
  );

  const organizationRows = organizations
    .map((organization) => {
      const organizationUnitRows = buildOrganizationUnitRows({
        organizationId: organization.id,
        organizationUnits,
        parentOrganizationUnitId: null,
        parentExternalId: organization.attributes.root_external_id,
        providerLookup,
        providersByOrganizationUnitId,
        useParentIdRelationships,
      });

      // Collect all provider IDs already placed inside OUs to avoid duplication
      // at the org level. This covers both relationship-based and fallback assignments.
      const providersInOus = new Set<string>();
      function collectOuProviderIds(rows: ProvidersTableRow[]) {
        for (const row of rows) {
          if (row.rowType === PROVIDERS_ROW_TYPE.PROVIDER) {
            providersInOus.add(row.id);
          } else {
            collectOuProviderIds(row.subRows);
          }
        }
      }
      collectOuProviderIds(organizationUnitRows);

      const organizationProvidersFromRelationships = getProviderRowsByIds({
        providerIds: getRelationshipProviderIds(organization.relationships),
        providerLookup,
      }).filter(
        (provider) =>
          !providersAssignedToOu.has(provider.id) &&
          !providersInOus.has(provider.id),
      );
      const organizationProviders =
        organizationProvidersFromRelationships.length > 0
          ? organizationProvidersFromRelationships
          : (providersByOrganizationId.get(organization.id) ?? []).filter(
              (provider) => !providersInOus.has(provider.id),
            );
      const subRows = [...organizationProviders, ...organizationUnitRows];
      const directProviderIds =
        organizationProvidersFromRelationships.length > 0
          ? getRelationshipProviderIds(organization.relationships)
          : organizationProviders.map((provider) => provider.id);
      const organizationUnitProviderIds =
        collectOrganizationRowProviderIds(organizationUnitRows);

      return createOrganizationRow({
        groupKind: PROVIDERS_GROUP_KIND.ORGANIZATION,
        id: organization.id,
        name: organization.attributes.name,
        externalId: organization.attributes.external_id,
        organizationId: organization.id,
        parentExternalId: organization.attributes.root_external_id,
        providerIds: dedupeIds([
          ...directProviderIds,
          ...organizationUnitProviderIds,
        ]),
        subRows,
      });
    })
    .filter((organizationRow) => organizationRow.providerIds.length > 0);

  const assignedProviderIds = new Set<string>();

  function collectAssignedProviderIds(rows: ProvidersTableRow[]) {
    for (const row of rows) {
      if (row.rowType === PROVIDERS_ROW_TYPE.PROVIDER) {
        assignedProviderIds.add(row.id);
        continue;
      }

      collectAssignedProviderIds(row.subRows);
    }
  }

  collectAssignedProviderIds(organizationRows);
  const orphanProviders = providers.filter(
    (provider) => !assignedProviderIds.has(provider.id),
  );

  return [...organizationRows, ...orphanProviders];
}

export async function loadProvidersAccountsViewData({
  isCloud,
  searchParams,
}: ProvidersAccountsViewInput): Promise<ProvidersAccountsViewData> {
  const page = parseInt(searchParams.page?.toString() ?? "1", 10);
  const pageSize = parseInt(searchParams.pageSize?.toString() ?? "10", 10);
  const { encodedSort } = extractSortAndKey(searchParams);
  const { filters, query } = extractFiltersAndQuery(searchParams);

  const providerFilters = { ...filters };

  // Map provider_type__in (used by ProviderTypeSelector) to provider__in (API param)
  const providerTypeFilter =
    providerFilters[PROVIDERS_FILTER_PARAM.PROVIDER_TYPE];
  if (providerTypeFilter) {
    providerFilters[PROVIDERS_FILTER_PARAM.PROVIDER] = providerTypeFilter;
  }

  delete providerFilters[PROVIDERS_FILTER_PARAM.PROVIDER_TYPE];

  const emptyOrganizationsResponse: OrganizationListResponse = {
    data: [],
  };
  const emptyOrganizationUnitsResponse: OrganizationUnitListResponse = {
    data: [],
  };

  const [
    providersResponse,
    allProvidersResponse,
    allProviderGroupsResponse,
    schedulesResponse,
    organizationsResponse,
    organizationUnitsResponse,
  ] = await Promise.all([
    resolveActionResult(
      getProviders({
        filters: providerFilters,
        page,
        pageSize,
        query,
        sort: encodedSort,
      }),
    ),
    // Unfiltered fetch for ProviderTypeSelector — only needs distinct types;
    // TODO: Replace with a dedicated lightweight endpoint when available.
    resolveActionResult(getAllProviders()),
    // Unfiltered fetch for the Provider Group selector dropdown.
    resolveActionResult(getAllProviderGroups()),
    // Fetch configured schedules as a fallback when provider scan_* fields are
    // absent (best-effort: typically empty in OSS).
    resolveActionResult(getSchedules()),
    isCloud
      ? listOrganizationsSafe()
      : Promise.resolve(emptyOrganizationsResponse),
    isCloud
      ? listOrganizationUnitsSafe()
      : Promise.resolve(emptyOrganizationUnitsResponse),
  ]);

  const schedulesByProviderId = buildSchedulesByProviderId(schedulesResponse);

  const orgs = organizationsResponse?.data ?? [];
  const ous = organizationUnitsResponse?.data ?? [];
  const providers = enrichProviders(providersResponse, schedulesByProviderId);

  const rows = buildProvidersTableRows({
    isCloud,
    organizations: orgs,
    organizationUnits: ous,
    providers,
  });

  return {
    filters: createProvidersFilters(),
    metadata: providersResponse?.meta,
    providers: allProvidersResponse?.data ?? [],
    providerGroups: allProviderGroupsResponse?.data ?? [],
    rows,
  };
}

export { PROVIDERS_ROW_TYPE };
