import {
  Injectable,
  computed,
  inject,
  signal
} from '@angular/core';
import { Store } from '@ngrx/store';
import {
  Event as RouterNavigationEvent,
  NavigationCancel,
  NavigationEnd,
  NavigationError,
  NavigationStart,
  Router
} from '@angular/router';

import { selectCurrentRoom } from '../../../store/rooms/rooms.selectors';
import { selectAllUsers, selectCurrentUser } from '../../../store/users/users.selectors';
import { DEBUG_LOG_MAX_ENTRIES, STORAGE_KEY_DEBUGGING_SETTINGS } from '../../constants';
import { buildDebugNetworkSnapshot } from './debugging-network-snapshot.builder';
import type {
  ConsoleMethod,
  ConsoleMethodName,
  DebugLogEntry,
  DebugLogLevel,
  DebugNetworkSnapshot,
  DebuggingSettingsState,
  PendingDebugEntry
} from '../../models/debugging.models';

@Injectable({ providedIn: 'root' })
export class DebuggingService {
  readonly enabled = signal(false);
  readonly entries = signal<DebugLogEntry[]>([]);
  readonly isConsoleOpen = signal(false);
  readonly networkSnapshot = computed<DebugNetworkSnapshot>(() =>
    buildDebugNetworkSnapshot(
      this.entries(),
      this.currentUser() ?? null,
      this.allUsers() ?? [],
      this.currentRoom() ?? null
    )
  );

  private readonly router = inject(Router);
  private readonly store = inject(Store);
  private readonly currentUser = this.store.selectSignal(selectCurrentUser);
  private readonly allUsers = this.store.selectSignal(selectAllUsers);
  private readonly currentRoom = this.store.selectSignal(selectCurrentRoom);
  private readonly timeFormatter = new Intl.DateTimeFormat(undefined, {
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    fractionalSecondDigits: 3,
    hour12: false
  });
  private readonly dateTimeFormatter = new Intl.DateTimeFormat(undefined, {
    year: 'numeric',
    month: 'short',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    fractionalSecondDigits: 3,
    hour12: false
  });
  private readonly originalConsoleMethods: Record<ConsoleMethodName, ConsoleMethod> = {
    debug: console.debug.bind(console),
    log: console.log.bind(console),
    info: console.info.bind(console),
    warn: console.warn.bind(console),
    error: console.error.bind(console)
  };

  private initialized = false;
  private nextEntryId = 1;
  private pendingEntries: PendingDebugEntry[] = [];
  private flushQueued = false;

  constructor() {
    this.loadSettings();
    this.init();
  }

  init(): void {
    if (this.initialized)
      return;

    this.initialized = true;
    this.patchConsole();
    this.trackRouterEvents();
    this.trackGlobalEvents();
  }

  setEnabled(enabled: boolean): void {
    if (enabled === this.enabled())
      return;

    if (enabled) {
      this.enabled.set(true);
      this.persistSettings();
      this.info('debugging', 'Debugging service enabled');
      return;
    }

    this.info('debugging', 'Debugging service disabled');
    this.enabled.set(false);
    this.isConsoleOpen.set(false);
    this.persistSettings();
  }

  toggleConsole(): void {
    if (!this.enabled())
      return;

    this.isConsoleOpen.update((open) => !open);
  }

  openConsole(): void {
    if (!this.enabled())
      return;

    this.isConsoleOpen.set(true);
  }

  closeConsole(): void {
    this.isConsoleOpen.set(false);
  }

  clear(): void {
    this.pendingEntries = [];
    this.entries.set([]);
  }

  recordEvent(source: string, message: string, payload?: unknown): void {
    if (!this.enabled())
      return;

    this.pushEntry({ level: 'event',
      source,
      message,
      payload });
  }

  info(source: string, message: string, payload?: unknown): void {
    if (!this.enabled())
      return;

    this.pushEntry({ level: 'info',
      source,
      message,
      payload });
  }

  warn(source: string, message: string, payload?: unknown): void {
    if (!this.enabled())
      return;

    this.pushEntry({ level: 'warn',
      source,
      message,
      payload });
  }

  error(source: string, message: string, payload?: unknown): void {
    if (!this.enabled())
      return;

    this.pushEntry({ level: 'error',
      source,
      message,
      payload });
  }

  private loadSettings(): void {
    try {
      const raw = localStorage.getItem(STORAGE_KEY_DEBUGGING_SETTINGS);

      if (!raw)
        return;

      const parsed = JSON.parse(raw) as DebuggingSettingsState;

      this.enabled.set(parsed.enabled === true);
    } catch {}
  }

  private persistSettings(): void {
    try {
      localStorage.setItem(
        STORAGE_KEY_DEBUGGING_SETTINGS,
        JSON.stringify({ enabled: this.enabled() })
      );
    } catch {}
  }

  private patchConsole(): void {
    this.patchConsoleMethod('debug', 'debug');
    this.patchConsoleMethod('log', 'info');
    this.patchConsoleMethod('info', 'info');
    this.patchConsoleMethod('warn', 'warn');
    this.patchConsoleMethod('error', 'error');
  }

  private patchConsoleMethod(methodName: ConsoleMethodName, level: DebugLogLevel): void {
    const originalMethod = this.originalConsoleMethods[methodName];
    const consoleRef = console as unknown as Record<ConsoleMethodName, ConsoleMethod>;

    consoleRef[methodName] = (...args: unknown[]) => {
      originalMethod(...args);
      this.captureConsoleMessage(level, args);
    };
  }

  private captureConsoleMessage(level: DebugLogLevel, args: readonly unknown[]): void {
    if (!this.enabled())
      return;

    const rawMessage = args.map((arg) => this.stringifyPreview(arg)).join(' ')
      .trim() || '(empty console call)';
    // Use only string args for label/message extraction so that
    // stringified object payloads don't pollute the parsed message.
    // Object payloads are captured separately via extractConsolePayload.
    const metadataSource = args
      .filter((arg): arg is string => typeof arg === 'string')
      .join(' ')
      .trim() || rawMessage;
    const consoleMetadata = this.extractConsoleMetadata(metadataSource);
    const payload = this.extractConsolePayload(args);
    const payloadText = payload === undefined
      ? null
      : this.stringifyPayload(payload);

    this.pushEntry({ level,
      source: consoleMetadata.source,
      message: consoleMetadata.message,
      payload,
      payloadText });
  }

  private extractConsoleMetadata(rawMessage: string): { source: string; message: string } {
    const labels: string[] = [];

    let remainder = rawMessage.trim();

    while (remainder.startsWith('[')) {
      const closingIndex = remainder.indexOf(']');

      if (closingIndex <= 1)
        break;

      labels.push(remainder.slice(1, closingIndex));
      remainder = remainder.slice(closingIndex + 1).trim();
    }

    if (labels.length === 0) {
      return {
        source: 'console',
        message: rawMessage
      };
    }

    const normalizedLabels = labels.map((label) => this.normalizeConsoleLabel(label));

    if (normalizedLabels[0] === 'webrtc') {
      return {
        source: normalizedLabels[1] ? `webrtc:${normalizedLabels[1]}` : 'webrtc',
        message: remainder || rawMessage
      };
    }

    return {
      source: normalizedLabels[0] || 'console',
      message: remainder || rawMessage
    };
  }

  private normalizeConsoleLabel(label: string): string {
    return label.trim().toLowerCase()
      .replace(/\s+/g, '-');
  }

  private extractConsolePayload(args: readonly unknown[]): unknown {
    const structuredArgs = args.filter((arg) => typeof arg !== 'string');

    if (structuredArgs.length === 0)
      return undefined;

    if (structuredArgs.length === 1)
      return structuredArgs[0];

    return structuredArgs;
  }

  private trackRouterEvents(): void {
    this.router.events.subscribe((event) => {
      if (!this.enabled())
        return;

      this.captureRouterEvent(event);
    });
  }

  private captureRouterEvent(event: RouterNavigationEvent): void {
    if (event instanceof NavigationStart) {
      this.pushEntry({ level: 'event',
        source: 'router',
        message: `Navigation started: ${event.url}`,
        payload: {
          id: event.id,
          url: event.url,
          navigationTrigger: event.navigationTrigger,
          restoredState: event.restoredState ?? null
        }
      });

      return;
    }

    if (event instanceof NavigationEnd) {
      this.pushEntry({ level: 'event',
        source: 'router',
        message: `Navigation completed: ${event.urlAfterRedirects}`,
        payload: {
          id: event.id,
          url: event.url,
          urlAfterRedirects: event.urlAfterRedirects
        }
      });

      return;
    }

    if (event instanceof NavigationCancel) {
      this.pushEntry({ level: 'warn',
        source: 'router',
        message: `Navigation cancelled: ${event.url}`,
        payload: {
          id: event.id,
          url: event.url,
          reason: event.reason,
          code: event.code
        }
      });

      return;
    }

    if (event instanceof NavigationError) {
      this.pushEntry({ level: 'error',
        source: 'router',
        message: `Navigation failed: ${event.url}`,
        payload: {
          id: event.id,
          url: event.url,
          error: event.error
        }
      });
    }
  }

  private trackGlobalEvents(): void {
    window.addEventListener('error', (event) => {
      if (!this.enabled())
        return;

      this.pushEntry({ level: 'error',
        source: 'window',
        message: event.message || 'Unhandled runtime error',
        payload: {
          filename: event.filename || null,
          line: event.lineno || null,
          column: event.colno || null,
          error: event.error
        }
      });
    });

    window.addEventListener('unhandledrejection', (event) => {
      if (!this.enabled())
        return;

      this.pushEntry({ level: 'error',
        source: 'window',
        message: 'Unhandled promise rejection',
        payload: event.reason
      });
    });

    window.addEventListener('online', () => {
      this.recordEvent('window', 'Browser connection restored');
    });

    window.addEventListener('offline', () => {
      this.warn('window', 'Browser went offline');
    });

    document.addEventListener('visibilitychange', () => {
      this.recordEvent('document', `Visibility changed: ${document.visibilityState}`, {
        visibilityState: document.visibilityState
      });
    });

    document.addEventListener('click', (event) => this.captureDocumentEvent(event), true);
    document.addEventListener('change', (event) => this.captureDocumentEvent(event), true);
    document.addEventListener('submit', (event) => this.captureDocumentEvent(event), true);
    document.addEventListener('keydown', (event) => this.captureDocumentEvent(event), true);
  }

  private captureDocumentEvent(event: Event): void {
    if (!this.enabled())
      return;

    if (this.isIgnoredTarget(event.target))
      return;

    if (event instanceof KeyboardEvent && !this.shouldTrackKeyboardEvent(event))
      return;

    const payload: Record<string, unknown> = {
      type: event.type,
      target: this.describeElement(event.target)
    };
    const controlMetadata = this.describeControl(event.target);

    if (controlMetadata)
      payload['control'] = controlMetadata;

    if (event instanceof KeyboardEvent) {
      payload['key'] = event.key;
      payload['code'] = event.code;
      payload['altKey'] = event.altKey;
      payload['ctrlKey'] = event.ctrlKey;
      payload['metaKey'] = event.metaKey;
      payload['shiftKey'] = event.shiftKey;
    }

    this.pushEntry({ level: 'event',
      source: 'ui',
      message: this.describeInteraction(event),
      payload });
  }

  private shouldTrackKeyboardEvent(event: KeyboardEvent): boolean {
    if (event.ctrlKey || event.metaKey || event.altKey)
      return true;

    return event.key === 'Enter' || event.key === 'Escape' || event.key === 'Tab';
  }

  private isIgnoredTarget(target: EventTarget | null): boolean {
    const element = this.asElement(target);

    if (!element)
      return false;

    return !!element.closest('[data-debug-console-root="true"]');
  }

  private describeInteraction(event: Event): string {
    const element = this.asElement(event.target);
    const targetLabel = element ? this.buildElementLabel(element) : 'unknown target';

    if (event instanceof KeyboardEvent)
      return `keydown (${event.key}) on ${targetLabel}`;

    return `${event.type} on ${targetLabel}`;
  }

  private buildElementLabel(element: Element): string {
    const tagName = element.tagName.toLowerCase();
    const parts = [tagName];
    const id = element.id.trim();
    const type = element.getAttribute('type');
    const name = element.getAttribute('name');
    const ariaLabel = element.getAttribute('aria-label');
    const placeholder = element.getAttribute('placeholder');
    const interactiveText = this.getInteractiveText(element);

    if (id)
      parts.push(`#${id}`);

    if (type)
      parts.push(`type=${type}`);

    if (name) {
      parts.push(`name=${name}`);
    } else if (ariaLabel) {
      parts.push(`label=${ariaLabel}`);
    } else if (placeholder) {
      parts.push(`placeholder=${placeholder}`);
    } else if (interactiveText) {
      parts.push(`text=${interactiveText}`);
    }

    return parts.join(' ');
  }

  private getInteractiveText(element: Element): string | null {
    const tagName = element.tagName.toLowerCase();

    if (tagName !== 'button' && tagName !== 'a')
      return null;

    const text = element.textContent?.trim().replace(/\s+/g, ' ') || '';

    if (!text)
      return null;

    return text.slice(0, 60);
  }

  private describeElement(target: EventTarget | null): Record<string, unknown> | null {
    const element = this.asElement(target);

    if (!element)
      return null;

    const payload: Record<string, unknown> = {
      tag: element.tagName.toLowerCase()
    };
    const id = element.id.trim();
    const type = element.getAttribute('type');
    const role = element.getAttribute('role');
    const name = element.getAttribute('name');
    const ariaLabel = element.getAttribute('aria-label');
    const placeholder = element.getAttribute('placeholder');
    const interactiveText = this.getInteractiveText(element);

    if (id)
      payload['id'] = id;

    if (type)
      payload['type'] = type;

    if (role)
      payload['role'] = role;

    if (name)
      payload['name'] = name;

    if (ariaLabel)
      payload['ariaLabel'] = ariaLabel;

    if (placeholder)
      payload['placeholder'] = placeholder;

    if (interactiveText)
      payload['text'] = interactiveText;

    return payload;
  }

  private describeControl(target: EventTarget | null): Record<string, unknown> | null {
    if (target instanceof HTMLInputElement) {
      if (target.type === 'checkbox' || target.type === 'radio') {
        return {
          type: target.type,
          checked: target.checked
        };
      }

      if (target.type === 'file') {
        return {
          type: target.type,
          filesCount: target.files?.length ?? 0
        };
      }

      return {
        type: target.type || 'text',
        hasValue: target.value.length > 0,
        valueLength: target.value.length,
        masked: target.type === 'password'
      };
    }

    if (target instanceof HTMLTextAreaElement) {
      return {
        type: 'textarea',
        hasValue: target.value.length > 0,
        valueLength: target.value.length
      };
    }

    if (target instanceof HTMLSelectElement) {
      return {
        type: 'select',
        hasValue: target.value.length > 0,
        selectedIndex: target.selectedIndex
      };
    }

    return null;
  }

  private asElement(target: EventTarget | null): Element | null {
    if (target instanceof Element)
      return target;

    if (target instanceof Node)
      return target.parentElement;

    return null;
  }

  private pushEntry(entry: PendingDebugEntry): void {
    this.pendingEntries.push(entry);
    this.schedulePendingEntryFlush();
  }

  private schedulePendingEntryFlush(): void {
    if (this.flushQueued)
      return;

    this.flushQueued = true;

    Promise.resolve().then(() => {
      this.flushQueued = false;
      this.flushPendingEntries();

      if (this.pendingEntries.length > 0)
        this.schedulePendingEntryFlush();
    });
  }

  private flushPendingEntries(): void {
    if (this.pendingEntries.length === 0)
      return;

    const pendingEntries = this.pendingEntries;

    this.pendingEntries = [];

    let nextEntries = this.entries();

    for (const entry of pendingEntries) {
      const nextEntry = this.createEntry(entry);
      const lastEntry = nextEntries[nextEntries.length - 1];

      if (lastEntry && this.canCollapseEntries(lastEntry, nextEntry)) {
        nextEntries = [
          ...nextEntries.slice(0, -1),
          {
            ...lastEntry,
            count: lastEntry.count + 1,
            timestamp: nextEntry.timestamp,
            timeLabel: nextEntry.timeLabel,
            dateTimeLabel: nextEntry.dateTimeLabel
          }
        ];

        continue;
      }

      nextEntries = [...nextEntries, nextEntry].slice(-DEBUG_LOG_MAX_ENTRIES);
    }

    this.entries.set(nextEntries);
  }

  private createEntry(entry: PendingDebugEntry): DebugLogEntry {
    const timestamp = entry.timestamp ?? Date.now();
    const hasPayload = entry.payload !== undefined;
    const normalizedPayload = hasPayload
      ? this.normalizePayload(entry.payload)
      : null;
    const payloadText = entry.payloadText === undefined
      ? (hasPayload ? this.stringifyNormalizedPayload(normalizedPayload) : null)
      : entry.payloadText;
    const nextEntry: DebugLogEntry = {
      id: this.nextEntryId++,
      timestamp,
      timeLabel: this.timeFormatter.format(timestamp),
      dateTimeLabel: this.dateTimeFormatter.format(timestamp),
      level: entry.level,
      source: entry.source,
      message: entry.message,
      payload: normalizedPayload,
      payloadText,
      count: 1
    };

    return nextEntry;
  }

  private canCollapseEntries(previousEntry: DebugLogEntry, nextEntry: DebugLogEntry): boolean {
    const withinWindow = nextEntry.timestamp - previousEntry.timestamp <= 1000;

    return withinWindow
      && previousEntry.level === nextEntry.level
      && previousEntry.source === nextEntry.source
      && previousEntry.message === nextEntry.message
      && previousEntry.payloadText === nextEntry.payloadText;
  }

  private stringifyPreview(value: unknown): string {
    if (typeof value === 'string')
      return value;

    if (value instanceof Error)
      return `${value.name}: ${value.message}`;

    const payloadText = this.stringifyPayload(value);

    if (!payloadText)
      return String(value);

    return payloadText.replace(/\s+/g, ' ').slice(0, 200);
  }

  private stringifyPayload(value: unknown): string | null {
    if (value === undefined)
      return null;

    return this.stringifyNormalizedPayload(this.normalizePayload(value));
  }

  private normalizePayload(value: unknown): unknown {
    try {
      return this.normalizeValue(value, 0, new WeakSet<object>());
    } catch {
      try {
        return String(value);
      } catch {
        return '[unserializable value]';
      }
    }
  }

  private stringifyNormalizedPayload(value: unknown): string | null {
    try {
      const json = JSON.stringify(value, null, 2);

      if (typeof json === 'string')
        return json;

      return value === undefined ? null : String(value);
    } catch {
      try {
        return String(value);
      } catch {
        return '[unserializable value]';
      }
    }
  }

  private normalizeValue(value: unknown, depth: number, seen: WeakSet<object>): unknown {
    if (
      value === null
      || typeof value === 'string'
      || typeof value === 'number'
      || typeof value === 'boolean'
    ) {
      return value;
    }

    if (typeof value === 'bigint')
      return value.toString();

    if (typeof value === 'function')
      return `[Function ${value.name || 'anonymous'}]`;

    if (value instanceof Date)
      return value.toISOString();

    if (value instanceof Error) {
      return {
        name: value.name,
        message: value.message,
        stack: value.stack
      };
    }

    if (value instanceof Event) {
      return {
        type: value.type,
        target: this.describeElement(value.target),
        currentTarget: this.describeElement(value.currentTarget),
        timeStamp: value.timeStamp
      };
    }

    if (value instanceof HTMLElement)
      return this.describeElement(value);

    if (typeof value !== 'object')
      return String(value);

    if (seen.has(value))
      return '[Circular]';

    seen.add(value);

    if (depth >= 3)
      return `[${value.constructor?.name || 'Object'}]`;

    if (Array.isArray(value)) {
      return value.slice(0, 20).map((item) => this.normalizeValue(item, depth + 1, seen));
    }

    const normalizedObject: Record<string, unknown> = {};
    const objectValue = value as Record<string, unknown>;

    for (const key of Object.keys(objectValue).slice(0, 20)) {
      normalizedObject[key] = this.normalizeValue(objectValue[key], depth + 1, seen);
    }

    return normalizedObject;
  }
}
