/* eslint-disable @typescript-eslint/no-unused-vars */
import type { ErrorClusterDoc } from './types.js';

// ============================================================================
// HDBSCAN (Hierarchical Density-Based Spatial Clustering) Implementation
// ============================================================================

interface DataPoint {
  id: string;
  embedding: number[];
  metadata: {
    errorType: string;
    messageTemplate: string;
    stackSignature: string;
    productId: string;
    firstSeenAt: string;
  };
}

interface Cluster {
  id: string;
  points: DataPoint[];
  centroid: number[];
  stability: number;
  density: number;
}

interface HDBSCANOptions {
  minClusterSize: number;
  minSamples: number;
  metric: 'euclidean' | 'cosine' | 'manhattan';
  alpha: number;
}

const DEFAULT_OPTIONS: HDBSCANOptions = {
  minClusterSize: 3,
  minSamples: 2,
  metric: 'cosine',
  alpha: 1.0,
};

// ============================================================================
// Distance Metrics
// ============================================================================

function calculateDistance(a: number[], b: number[], metric: string): number {
  switch (metric) {
    case 'euclidean':
      return euclideanDistance(a, b);
    case 'cosine':
      return 1 - cosineSimilarity(a, b); // Convert similarity to distance
    case 'manhattan':
      return manhattanDistance(a, b);
    default:
      return euclideanDistance(a, b);
  }
}

function euclideanDistance(a: number[], b: number[]): number {
  let sum = 0;
  for (let i = 0; i < a.length; i++) {
    const diff = a[i] - b[i];
    sum += diff * diff;
  }
  return Math.sqrt(sum);
}

function cosineSimilarity(a: number[], b: number[]): number {
  if (a.length !== b.length) return 0;

  let dotProduct = 0;
  let normA = 0;
  let normB = 0;

  for (let i = 0; i < a.length; i++) {
    dotProduct += a[i] * b[i];
    normA += a[i] * a[i];
    normB += b[i] * b[i];
  }

  if (normA === 0 || normB === 0) return 0;

  return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}

function manhattanDistance(a: number[], b: number[]): number {
  let sum = 0;
  for (let i = 0; i < a.length; i++) {
    sum += Math.abs(a[i] - b[i]);
  }
  return sum;
}

// ============================================================================
// Mutual Reachability Distance
// ============================================================================

interface ReachabilityResult {
  reachabilityDistances: number[][];
  coreDistances: number[];
}

/**
 * Computes mutual reachability distances for HDBSCAN
 * This combines the original distance with core distances to handle varying densities
 */
function computeMutualReachability(
  points: DataPoint[],
  minSamples: number,
  metric: string
): ReachabilityResult {
  const n = points.length;
  const distances: number[][] = Array(n)
    .fill(null)
    .map(() => Array(n).fill(0));
  const coreDistances: number[] = new Array(n);

  // Compute pairwise distances
  for (let i = 0; i < n; i++) {
    for (let j = i + 1; j < n; j++) {
      const dist = calculateDistance(points[i].embedding, points[j].embedding, metric);
      distances[i][j] = dist;
      distances[j][i] = dist;
    }
  }

  // Compute core distances (distance to minSamples-th nearest neighbor)
  for (let i = 0; i < n; i++) {
    const sortedDists = [...distances[i]].sort((a, b) => a - b);
    coreDistances[i] = sortedDists[Math.min(minSamples, sortedDists.length - 1)] || 0;
  }

  // Compute mutual reachability distances
  const reachabilityDistances: number[][] = Array(n)
    .fill(null)
    .map(() => Array(n).fill(0));

  for (let i = 0; i < n; i++) {
    for (let j = i + 1; j < n; j++) {
      const mutualDist = Math.max(coreDistances[i], coreDistances[j], distances[i][j]);
      reachabilityDistances[i][j] = mutualDist;
      reachabilityDistances[j][i] = mutualDist;
    }
  }

  return { reachabilityDistances, coreDistances };
}

// ============================================================================
// Minimum Spanning Tree (Prim's Algorithm)
// ============================================================================

interface Edge {
  from: number;
  to: number;
  weight: number;
}

function computeMST(distances: number[][]): Edge[] {
  const n = distances.length;
  const visited = new Set<number>();
  const mst: Edge[] = [];

  // Start from node 0
  visited.add(0);

  while (visited.size < n) {
    let minEdge: Edge | null = null;

    for (const i of visited) {
      for (let j = 0; j < n; j++) {
        if (!visited.has(j)) {
          const weight = distances[i][j];
          if (minEdge === null || weight < minEdge.weight) {
            minEdge = { from: i, to: j, weight };
          }
        }
      }
    }

    if (minEdge === null) break;

    mst.push(minEdge);
    visited.add(minEdge.to);
  }

  return mst;
}

// ============================================================================
// Cluster Hierarchy (Single Linkage)
// ============================================================================

interface HierarchyNode {
  left: number | HierarchyNode;
  right: number | HierarchyNode;
  distance: number;
  size: number;
}

function buildHierarchy(mst: Edge[], n: number): HierarchyNode {
  // Sort edges by weight
  const sortedEdges = [...mst].sort((a, b) => a.weight - b.weight);

  // Union-Find for tracking merges
  const parent = new Array(n).fill(0).map((_, i) => i);
  const size = new Array(n).fill(1);
  const clusters: Map<number, HierarchyNode> = new Map();

  function find(x: number): number {
    if (parent[x] !== x) {
      parent[x] = find(parent[x]);
    }
    return parent[x];
  }

  function union(x: number, y: number): number {
    const px = find(x);
    const py = find(y);
    if (px === py) return px;

    if (size[px] < size[py]) {
      parent[px] = py;
      size[py] += size[px];
      return py;
    } else {
      parent[py] = px;
      size[px] += size[py];
      return px;
    }
  }

  const nextClusterId = n;

  for (const edge of sortedEdges) {
    const left = find(edge.from);
    const right = find(edge.to);

    if (left !== right) {
      const leftCluster = clusters.get(left) ?? left;
      const rightCluster = clusters.get(right) ?? right;

      const newCluster: HierarchyNode = {
        left: leftCluster,
        right: rightCluster,
        distance: edge.weight,
        size: size[left] + size[right],
      };

      const newId = union(left, right);
      clusters.set(newId, newCluster);
      clusters.delete(left);
      clusters.delete(right);
      clusters.set(newId, newCluster);
    }
  }

  // Return the root cluster
  const rootId = find(0);
  return clusters.get(rootId) ?? { left: 0, right: 1, distance: 0, size: n };
}

// ============================================================================
// Extract Clusters from Hierarchy
// ============================================================================

function extractClusters(
  node: HierarchyNode | number,
  points: DataPoint[],
  minClusterSize: number,
  currentDistance: number = 0
): { clusters: DataPoint[][]; stabilities: number[] } {
  if (typeof node === 'number') {
    // Leaf node (single point)
    return { clusters: [[points[node]]], stabilities: [1] };
  }

  // Check if this node should be a cluster
  if (node.size >= minClusterSize) {
    // Extract all points in this cluster
    const allPoints = collectPoints(node, points);
    return { clusters: [allPoints], stabilities: [calculateStability(node)] };
  }

  // Otherwise, recursively extract from children
  const leftResult = extractClusters(node.left, points, minClusterSize, node.distance);
  const rightResult = extractClusters(node.right, points, minClusterSize, node.distance);

  return {
    clusters: [...leftResult.clusters, ...rightResult.clusters],
    stabilities: [...leftResult.stabilities, ...rightResult.stabilities],
  };
}

function collectPoints(node: HierarchyNode | number, points: DataPoint[]): DataPoint[] {
  if (typeof node === 'number') {
    return [points[node]];
  }
  return [...collectPoints(node.left, points), ...collectPoints(node.right, points)];
}

function calculateStability(node: HierarchyNode): number {
  // Stability is based on how long the cluster persists in the hierarchy
  if (typeof node === 'number') return 1;

  // Simple stability: inversely proportional to the distance at which it forms
  // Clusters that form at lower distances (denser regions) are more stable
  const baseStability = node.distance > 0 ? 1 / node.distance : 1;
  const sizeBonus = Math.log(node.size) / Math.log(2); // log2(size)

  return baseStability * sizeBonus;
}

// ============================================================================
// Main HDBSCAN Algorithm
// ============================================================================

export interface HDBSCANResult {
  clusters: Cluster[];
  noise: DataPoint[];
  labels: Map<string, number>; // point id -> cluster index or -1 for noise
}

/**
 * Runs HDBSCAN clustering on error embeddings
 */
export function runHDBSCAN(
  points: DataPoint[],
  options: Partial<HDBSCANOptions> = {}
): HDBSCANResult {
  const opts = { ...DEFAULT_OPTIONS, ...options };

  if (points.length < opts.minClusterSize) {
    return {
      clusters: [],
      noise: points,
      labels: new Map(points.map(p => [p.id, -1])),
    };
  }

  // Step 1: Compute mutual reachability distances
  const { reachabilityDistances } = computeMutualReachability(points, opts.minSamples, opts.metric);

  // Step 2: Compute Minimum Spanning Tree
  const mst = computeMST(reachabilityDistances);

  // Step 3: Build hierarchy via single linkage
  const hierarchy = buildHierarchy(mst, points.length);

  // Step 4: Extract clusters from hierarchy
  const { clusters: rawClusters } = extractClusters(hierarchy, points, opts.minClusterSize);

  // Step 5: Calculate centroids and format results
  const clusters: Cluster[] = rawClusters.map((clusterPoints, index) => ({
    id: `cluster_${index}_${Date.now()}`,
    points: clusterPoints,
    centroid: calculateCentroid(clusterPoints.map(p => p.embedding)),
    stability: calculateClusterStability(clusterPoints),
    density: clusterPoints.length / averagePairwiseDistance(clusterPoints),
  }));

  // Step 6: Identify noise points (points not in any cluster)
  const clusteredIds = new Set(clusters.flatMap(c => c.points.map(p => p.id)));
  const noise = points.filter(p => !clusteredIds.has(p.id));

  // Step 7: Assign labels
  const labels = new Map<string, number>();
  clusters.forEach((cluster, idx) => {
    cluster.points.forEach(p => labels.set(p.id, idx));
  });
  noise.forEach(p => labels.set(p.id, -1));

  return { clusters, noise, labels };
}

function calculateCentroid(embeddings: number[][]): number[] {
  if (embeddings.length === 0) return [];

  const dimensions = embeddings[0].length;
  const centroid = new Array(dimensions).fill(0);

  for (const emb of embeddings) {
    for (let i = 0; i < dimensions; i++) {
      centroid[i] += emb[i];
    }
  }

  for (let i = 0; i < dimensions; i++) {
    centroid[i] /= embeddings.length;
  }

  return centroid;
}

function calculateClusterStability(points: DataPoint[]): number {
  // Cluster stability based on temporal coherence
  // Errors that occur close together in time are more likely to be related
  if (points.length < 2) return 1;

  const timestamps = points
    .map(p => new Date(p.metadata.firstSeenAt).getTime())
    .sort((a, b) => a - b);

  const timeSpan = timestamps[timestamps.length - 1] - timestamps[0];
  const avgInterval = timeSpan / (points.length - 1);

  // Higher stability for tighter temporal clustering (smaller intervals)
  const maxExpectedInterval = 7 * 24 * 60 * 60 * 1000; // 7 days in ms
  return Math.max(0, 1 - avgInterval / maxExpectedInterval);
}

function averagePairwiseDistance(points: DataPoint[]): number {
  if (points.length < 2) return 1;

  let totalDist = 0;
  let count = 0;

  for (let i = 0; i < points.length; i++) {
    for (let j = i + 1; j < points.length; j++) {
      totalDist += euclideanDistance(points[i].embedding, points[j].embedding);
      count++;
    }
  }

  return count > 0 ? totalDist / count : 1;
}

// ============================================================================
// DBSCAN Fallback for Smaller Datasets
// ============================================================================

interface DBSCANOptions {
  eps: number;
  minPts: number;
}

export function runDBSCAN(points: DataPoint[], options: DBSCANOptions): HDBSCANResult {
  const { eps, minPts } = options;
  const n = points.length;
  const visited = new Set<number>();
  const clustered = new Map<number, number>(); // point index -> cluster id
  let clusterId = 0;

  function regionQuery(pointIdx: number): number[] {
    const neighbors: number[] = [];
    for (let i = 0; i < n; i++) {
      if (i !== pointIdx) {
        const dist = euclideanDistance(points[pointIdx].embedding, points[i].embedding);
        if (dist <= eps) {
          neighbors.push(i);
        }
      }
    }
    return neighbors;
  }

  function expandCluster(pointIdx: number, neighbors: number[], currentClusterId: number): void {
    clustered.set(pointIdx, currentClusterId);

    for (let i = 0; i < neighbors.length; i++) {
      const neighborIdx = neighbors[i];

      if (!visited.has(neighborIdx)) {
        visited.add(neighborIdx);
        const neighborNeighbors = regionQuery(neighborIdx);

        if (neighborNeighbors.length >= minPts) {
          neighbors.push(...neighborNeighbors);
        }
      }

      if (!clustered.has(neighborIdx)) {
        clustered.set(neighborIdx, currentClusterId);
      }
    }
  }

  for (let i = 0; i < n; i++) {
    if (visited.has(i)) continue;

    visited.add(i);
    const neighbors = regionQuery(i);

    if (neighbors.length < minPts) {
      // Mark as noise (will remain unclustered)
    } else {
      expandCluster(i, neighbors, clusterId);
      clusterId++;
    }
  }

  // Format results
  const clusters: Cluster[] = [];
  for (let cid = 0; cid < clusterId; cid++) {
    const clusterPoints = points.filter((_, idx) => clustered.get(idx) === cid);
    if (clusterPoints.length > 0) {
      clusters.push({
        id: `cluster_${cid}_${Date.now()}`,
        points: clusterPoints,
        centroid: calculateCentroid(clusterPoints.map(p => p.embedding)),
        stability: calculateClusterStability(clusterPoints),
        density: clusterPoints.length / averagePairwiseDistance(clusterPoints),
      });
    }
  }

  const noise = points.filter((_, idx) => !clustered.has(idx));
  const labels = new Map<string, number>();
  points.forEach((p, idx) => {
    labels.set(p.id, clustered.get(idx) ?? -1);
  });

  return { clusters, noise, labels };
}

// ============================================================================
// Auto-parameter Selection
// ============================================================================

/**
 * Automatically selects HDBSCAN parameters based on dataset size
 */
export function autoSelectParameters(
  datasetSize: number
): Partial<HDBSCANOptions> & { algorithm: 'hdbscan' | 'dbscan' } {
  if (datasetSize < 10) {
    // Use DBSCAN for very small datasets
    return {
      algorithm: 'dbscan',
      eps: 0.5,
      minPts: 2,
    } as Partial<HDBSCANOptions> & { algorithm: 'dbscan' };
  }

  if (datasetSize < 50) {
    // Small dataset: use HDBSCAN with small min cluster size
    return {
      algorithm: 'hdbscan',
      minClusterSize: 2,
      minSamples: 1,
      metric: 'cosine',
      alpha: 1.0,
    };
  }

  if (datasetSize < 500) {
    // Medium dataset
    return {
      algorithm: 'hdbscan',
      minClusterSize: 3,
      minSamples: 2,
      metric: 'cosine',
      alpha: 1.0,
    };
  }

  // Large dataset
  return {
    algorithm: 'hdbscan',
    minClusterSize: 5,
    minSamples: 3,
    metric: 'cosine',
    alpha: 1.0,
  };
}

// ============================================================================
// Cluster Quality Metrics
// ============================================================================

interface ClusterQualityMetrics {
  silhouetteScore: number;
  daviesBouldinIndex: number;
  calinskiHarabaszIndex: number;
}

export function calculateClusterQuality(
  points: DataPoint[],
  labels: Map<string, number>,
  clusters: Cluster[]
): ClusterQualityMetrics {
  // Simplified silhouette score calculation
  const silhouetteScores: number[] = [];

  for (const point of points) {
    const label = labels.get(point.id) ?? -1;
    if (label === -1) continue; // Skip noise points

    // Average distance to points in same cluster (cohesion)
    const sameCluster = points.filter(p => labels.get(p.id) === label && p.id !== point.id);
    const a =
      sameCluster.length > 0
        ? sameCluster.reduce((sum, p) => sum + euclideanDistance(point.embedding, p.embedding), 0) /
          sameCluster.length
        : 0;

    // Minimum average distance to points in different clusters (separation)
    let b = Infinity;
    for (let i = 0; i < clusters.length; i++) {
      if (i === label) continue;
      const otherCluster = points.filter(p => labels.get(p.id) === i);
      if (otherCluster.length === 0) continue;

      const avgDist =
        otherCluster.reduce((sum, p) => sum + euclideanDistance(point.embedding, p.embedding), 0) /
        otherCluster.length;
      b = Math.min(b, avgDist);
    }

    if (b === Infinity) b = a; // No other clusters

    const silhouette = (b - a) / Math.max(a, b);
    silhouetteScores.push(silhouette);
  }

  const avgSilhouette =
    silhouetteScores.length > 0
      ? silhouetteScores.reduce((sum, s) => sum + s, 0) / silhouetteScores.length
      : 0;

  return {
    silhouetteScore: avgSilhouette,
    daviesBouldinIndex: 0, // Simplified - would need full implementation
    calinskiHarabaszIndex: 0, // Simplified - would need full implementation
  };
}
