import {
  ApplicationRef,
  inject,
  Injectable
} from '@angular/core';
import { Store } from '@ngrx/store';
import { Router } from '@angular/router';
import type { AppState } from '../../store';
import { mapStoreSliceToDomain } from './domain-mapping.rules';
import { estimateStructuredBytes } from './state-size.rules';
import { scanComponentTree } from './component-tree.scanner';
import type { PerfDiagEntry, PerfDiagReporter } from './diagnostics.models';

const SAMPLE_BUDGET_MS = 8;

export interface RendererDiagnosticsSample {
  storeDomains: Record<string, number>;
  storeBytes: Record<string, number>;
  components: Record<string, number>;
  componentDomains: Record<string, number>;
  suspectedLeaks: { name: string; count: number; expected: number }[];
  heap: {
    usedJsHeapMb: number | null;
    totalJsHeapMb: number | null;
  };
  route: string | null;
  durationMs: number;
}

@Injectable({ providedIn: 'root' })
export class PerfDiagnosticsCollector {
  private readonly appRef = inject(ApplicationRef);
  private readonly store = inject(Store<AppState>);
  private readonly router = inject(Router);
  private disabled = false;
  private readonly expectedComponentCounts: Record<string, number> = {
    App: 1
  };

  collectSample(): RendererDiagnosticsSample | null {
    if (this.disabled) {
      return null;
    }

    const startedAt = performance.now();

    try {
      const state = this.collectStoreState();
      const storeBytes: Record<string, number> = {};
      const storeDomains: Record<string, number> = {};

      for (const [sliceKey, sliceValue] of Object.entries(state)) {
        const bytes = estimateStructuredBytes(sliceValue, { maxNodes: 200, maxDepth: 5 });
        const domain = mapStoreSliceToDomain(sliceKey);

        storeBytes[sliceKey] = bytes;
        storeDomains[domain] = (storeDomains[domain] ?? 0) + bytes;
      }

      const componentScan = scanComponentTree(this.appRef, this.expectedComponentCounts);
      const heap = readJsHeapSnapshot();

      return {
        storeDomains,
        storeBytes,
        components: componentScan.components,
        componentDomains: componentScan.domains,
        suspectedLeaks: componentScan.suspectedLeaks,
        heap,
        route: this.router.url,
        durationMs: performance.now() - startedAt
      };
    } catch {
      this.disabled = true;
      return null;
    } finally {
      if (performance.now() - startedAt > SAMPLE_BUDGET_MS) {
        // Skip future samples if this collector cannot stay within budget.
        this.disabled = true;
      }
    }
  }

  buildEntries(sample: RendererDiagnosticsSample): PerfDiagEntry[] {
    const collectedAt = Date.now();

    return [
      {
        collectedAt,
        source: 'renderer',
        type: 'store',
        payload: {
          domains: sample.storeDomains,
          slices: sample.storeBytes,
          route: sample.route
        }
      },
      {
        collectedAt,
        source: 'renderer',
        type: 'components',
        payload: {
          components: sample.components,
          domains: sample.componentDomains,
          suspectedLeaks: sample.suspectedLeaks,
          route: sample.route,
          durationMs: sample.durationMs
        }
      },
      {
        collectedAt,
        source: 'renderer',
        type: 'heap',
        payload: {
          ...sample.heap,
          route: sample.route
        }
      }
    ];
  }

  private collectStoreState(): AppState {
    let latest: AppState | undefined;

    this.store.subscribe((state) => {
      latest = state;
    }).unsubscribe();

    if (!latest) {
      throw new Error('Store state unavailable');
    }

    return latest;
  }
}

export async function publishRendererDiagnosticsSample(
  reporter: PerfDiagReporter,
  collector: PerfDiagnosticsCollector
): Promise<boolean> {
  const sample = collector.collectSample();

  if (!sample) {
    return false;
  }

  const entries = collector.buildEntries(sample);

  let reported = true;

  for (const entry of entries) {
    const accepted = await reporter.report(entry);

    if (!accepted) {
      reported = false;
    }
  }

  return reported;
}

function readJsHeapSnapshot(): { usedJsHeapMb: number | null; totalJsHeapMb: number | null } {
  const memory = (performance as Performance & {
    memory?: {
      usedJSHeapSize: number;
      totalJSHeapSize: number;
    };
  }).memory;

  if (!memory) {
    return {
      usedJsHeapMb: null,
      totalJsHeapMb: null
    };
  }

  return {
    usedJsHeapMb: roundMb(memory.usedJSHeapSize),
    totalJsHeapMb: roundMb(memory.totalJSHeapSize)
  };
}

function roundMb(bytes: number): number {
  return Math.round((bytes / (1024 * 1024)) * 100) / 100;
}
