import type {
  ComponentId,
  Fact,
  FactId,
  UsageComponentFact,
  UsageInlineStyleFact,
  UsageNodeFact,
  UsagePropResolvedFact,
  UsageTextChildFact,
} from "@fragments-sdk/core";

import type {
  ComponentCallSite,
  ComponentUsageAnalysis,
  ComponentUsageSummary,
  ObservedProp,
} from "./types.js";

export function aggregateComponentUsage(
  facts: readonly Fact[],
): ComponentUsageAnalysis {
  const nodes = new Map<FactId, UsageNodeFact>();
  const componentsByNode = new Map<FactId, UsageComponentFact>();
  const propsByNode = new Map<FactId, UsagePropResolvedFact[]>();
  const inlineStylesByNode = new Map<FactId, UsageInlineStyleFact[]>();
  const textChildrenByNode = new Map<FactId, UsageTextChildFact[]>();

  for (const fact of facts) {
    switch (fact.kind) {
      case "usage_node":
        nodes.set(fact.id, fact);
        break;
      case "usage_component":
        componentsByNode.set(fact.nodeId, fact);
        break;
      case "usage_prop_resolved":
        pushByNode(propsByNode, fact.nodeId, fact);
        break;
      case "usage_inline_style":
        pushByNode(inlineStylesByNode, fact.nodeId, fact);
        break;
      case "usage_text_child":
        pushByNode(textChildrenByNode, fact.nodeId, fact);
        break;
    }
  }

  const callSites: ComponentCallSite[] = [];

  for (const [nodeId, component] of componentsByNode) {
    const node = nodes.get(nodeId);
    if (!node) continue;

    callSites.push({
      componentId: component.componentId,
      element: node.element,
      filePath: node.file,
      line: node.location.line,
      column: node.location.column,
      nodePath: node.nodePath,
      props: (propsByNode.get(nodeId) ?? [])
        .map(toObservedProp)
        .filter(isProductProp),
      inlineStyles: (inlineStylesByNode.get(nodeId) ?? []).map((style) => ({
        property: style.property,
        valueKind: style.valueKind,
        value: style.value,
      })),
      textChildren: (textChildrenByNode.get(nodeId) ?? [])
        .sort((a, b) => a.index - b.index)
        .map((child) => child.text),
    });
  }

  callSites.sort(compareCallSites);

  return {
    callSites,
    summaries: summarizeCallSites(callSites),
  };
}

function pushByNode<T>(
  map: Map<FactId, T[]>,
  nodeId: FactId,
  value: T,
): void {
  const bucket = map.get(nodeId) ?? [];
  bucket.push(value);
  map.set(nodeId, bucket);
}

function toObservedProp(fact: UsagePropResolvedFact): ObservedProp {
  if (fact.resolution === "spread") {
    return {
      name: fact.prop,
      kind: "spread",
      location: fact.location,
    };
  }
  if (fact.resolution === "dynamic") {
    return {
      name: fact.prop,
      kind: "dynamic",
      location: fact.location,
    };
  }
  if (fact.resolution === "jsx") {
    return {
      name: fact.prop,
      kind: "jsx",
      location: fact.location,
    };
  }
  if (fact.value === null) {
    return {
      name: fact.prop,
      kind: "null",
      value: null,
      location: fact.location,
    };
  }
  if (typeof fact.value === "boolean") {
    return {
      name: fact.prop,
      kind: "boolean",
      value: fact.value,
      location: fact.location,
    };
  }
  return {
    name: fact.prop,
    kind: "static",
    value: printableValue(fact.value),
    location: fact.location,
  };
}

function isProductProp(prop: ObservedProp): boolean {
  return prop.name !== "key";
}

function printableValue(value: unknown): string | number | boolean | null | undefined {
  if (
    typeof value === "string" ||
    typeof value === "number" ||
    typeof value === "boolean" ||
    value === null ||
    value === undefined
  ) {
    return value;
  }
  return JSON.stringify(value);
}

function summarizeCallSites(
  callSites: readonly ComponentCallSite[],
): ComponentUsageSummary[] {
  const byComponent = new Map<ComponentId, ComponentCallSite[]>();
  for (const callSite of callSites) {
    const bucket = byComponent.get(callSite.componentId) ?? [];
    bucket.push(callSite);
    byComponent.set(callSite.componentId, bucket);
  }

  return [...byComponent.entries()]
    .sort(([a], [b]) => String(a).localeCompare(String(b)))
    .map(([componentId, sites]) => {
      const files = new Set(sites.map((site) => site.filePath));
      const propCounts: Record<string, number> = {};
      const staticValueCounts: Record<string, Record<string, number>> = {};
      const dynamicPropCounts: Record<string, number> = {};
      let spreadCount = 0;

      for (const site of sites) {
        for (const prop of site.props) {
          if (prop.kind === "spread") {
            spreadCount++;
            continue;
          }
          propCounts[prop.name] = (propCounts[prop.name] ?? 0) + 1;
          if (prop.kind === "dynamic") {
            dynamicPropCounts[prop.name] =
              (dynamicPropCounts[prop.name] ?? 0) + 1;
            continue;
          }
          if (prop.kind === "jsx") {
            continue;
          }
          const valueKey = String(prop.value);
          const values = staticValueCounts[prop.name] ?? {};
          values[valueKey] = (values[valueKey] ?? 0) + 1;
          staticValueCounts[prop.name] = values;
        }
      }

      return {
        componentId,
        callSiteCount: sites.length,
        importingFileCount: files.size,
        propCounts,
        staticValueCounts,
        dynamicPropCounts,
        spreadCount,
      };
    });
}

function compareCallSites(a: ComponentCallSite, b: ComponentCallSite): number {
  return (
    String(a.componentId).localeCompare(String(b.componentId)) ||
    a.filePath.localeCompare(b.filePath) ||
    a.line - b.line ||
    a.column - b.column ||
    a.nodePath.localeCompare(b.nodePath)
  );
}
