import {
  Injectable,
  NgZone,
  OnDestroy,
  inject
} from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Store } from '@ngrx/store';
import { Subscription, firstValueFrom } from 'rxjs';
import { ElectronBridgeService } from '../../../core/platform/electron/electron-bridge.service';
import { RealtimeSessionFacade } from '../../../core/realtime';
import { ServerDirectoryFacade } from '../../server-directory';
import { UsersActions } from '../../../store/users/users.actions';
import { selectAllUsers, selectCurrentUser } from '../../../store/users/users.selectors';
import type {
  ChatEvent,
  GameActivity,
  GameStoreLink,
  GameMatchResponse,
  MatchedGame,
  User
} from '../../../shared-kernel';

const DEFAULT_SCAN_INTERVAL_MS = 10_000;
const MIN_SCAN_INTERVAL_MS = 5_000;
const MAX_SCAN_INTERVAL_MS = 60_000;
const MAX_PROCESS_NAMES_PER_REQUEST = 256;
const MAX_CANDIDATE_PROCESSES_PER_REQUEST = 12;
const POSITIVE_CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
const NEGATIVE_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
const MAX_LOCAL_CACHE_ENTRIES = 128;
const SCAN_INTERVAL_STORAGE_KEY = 'metoyou_game_scan_interval_ms';
const GAME_MATCH_CACHE_STORAGE_KEY = 'metoyou_game_match_cache_v1';

interface CachedGameMatch {
  expiresAt: number;
  game: MatchedGame | null;
}

interface CandidateProcess {
  processName: string;
  score: number;
}

const IGNORED_PROCESS_NAMES = new Set([
  'agent',
  'bash',
  'baloorunner',
  'chrome',
  'code',
  'conhost',
  'cursor',
  'csrss',
  'dbus daemon',
  'discord',
  'dwm',
  'electron',
  'explorer',
  'firefox',
  'gameoverlayui',
  'gamemoded',
  'gamescopereaper',
  'gnome shell',
  'metoyou',
  'node',
  'npm',
  'powershell',
  'pulseaudio',
  'steam',
  'steamwebhelper',
  'systemd',
  'taskhostw',
  'wininit',
  'winlogon',
  'xorg'
]);
const IGNORED_PROCESS_PATTERNS = [
  new RegExp('(^|\\s)(agent|browser|daemon|desktop|helper|indexer|launcher|monitor|renderer|runner)(\\s|$)'),
  new RegExp('(^|\\s)(service|settings|shell|tray|updater|utility|watcher|worker)(\\s|$)'),
  new RegExp('(^|\\s)(audio|bluetooth|clipboard|crash|dbus|file|gpu|input|network|notification)(\\s|$)'),
  new RegExp('(^|\\s)(portal|proxy|screen|session|sync|system|tracker|web|window)(\\s|$)'),
  /^(appimage|at spi|baloo|dconf|gvfs|ibus|kde|kworker)/,
  /^(pipewire|plasmashell|pulseaudio|xdg|xwayland|zeitgeist)/,
  /(helper|service|daemon|runner|tracker|portal|updater|worker)$/
];

@Injectable({ providedIn: 'root' })
export class GameActivityService implements OnDestroy {
  private readonly electron = inject(ElectronBridgeService);
  private readonly http = inject(HttpClient);
  private readonly ngZone = inject(NgZone);
  private readonly serverDirectory = inject(ServerDirectoryFacade);
  private readonly store = inject(Store);
  private readonly webrtc = inject(RealtimeSessionFacade);

  private readonly currentUser = this.store.selectSignal(selectCurrentUser);
  private readonly allUsers = this.store.selectSignal(selectAllUsers);
  private readonly subscriptions = new Subscription();

  private scanTimer: ReturnType<typeof setInterval> | null = null;
  private lastProcessHash = '';
  private currentActivity: GameActivity | null = null;
  private scanInFlight = false;
  private started = false;

  start(): void {
    if (this.started) {
      return;
    }

    this.started = true;

    this.subscriptions.add(
      this.webrtc.onMessageReceived.subscribe((event) => this.handlePeerEvent(event))
    );

    this.subscriptions.add(
      this.webrtc.onPeerConnected.subscribe((peerId) => this.sendCurrentActivityToPeer(peerId))
    );

    const api = this.electron.getApi();

    if (!api?.getRunningProcessNames) {
      return;
    }

    this.ngZone.runOutsideAngular(() => {
      this.scanTimer = setInterval(() => {
        void this.scanRunningProcesses();
      }, this.getScanIntervalMs());
    });

    void this.scanRunningProcesses();
  }

  ngOnDestroy(): void {
    this.stop();
  }

  private stop(): void {
    if (this.scanTimer) {
      clearInterval(this.scanTimer);
      this.scanTimer = null;
    }

    this.subscriptions.unsubscribe();
    this.started = false;
  }

  private async scanRunningProcesses(): Promise<void> {
    if (this.scanInFlight || !this.currentUser()) {
      return;
    }

    const api = this.electron.getApi();

    if (!api?.getRunningProcessNames) {
      return;
    }

    this.scanInFlight = true;

    try {
      const processNames = (await api.getRunningProcessNames()).slice(0, MAX_PROCESS_NAMES_PER_REQUEST);
      const processHash = this.buildProcessHash(processNames);

      if (processHash === this.lastProcessHash) {
        return;
      }

      this.lastProcessHash = processHash;

      const matchedGame = await this.matchRunningGame(processNames);

      this.ngZone.run(() => this.applyMatchedGame(matchedGame));
    } catch (error) {
      console.warn('[GameActivity] Failed to scan running processes', error);
      return;
    } finally {
      this.scanInFlight = false;
    }
  }

  private async matchRunningGame(processes: string[]): Promise<MatchedGame | null> {
    const candidates = this.selectCandidateProcesses(processes);
    const cachedGame = this.findCachedGame(candidates);

    if (cachedGame !== undefined) {
      return cachedGame;
    }

    const unknownCandidates = candidates
      .filter((candidate) => !this.hasFreshCacheEntry(candidate.processName))
      .slice(0, MAX_CANDIDATE_PROCESSES_PER_REQUEST);

    if (unknownCandidates.length === 0) {
      return null;
    }

    const apiBase = this.serverDirectory.getApiBaseUrl();
    const currentUser = this.currentUser();
    const response = await firstValueFrom(
      this.http.post<GameMatchResponse>(`${apiBase}/games/match`, {
        processes: unknownCandidates.map((candidate) => candidate.processName),
        userId: currentUser?.id ?? currentUser?.oderId
      })
    );

    this.storeMatchResponse(unknownCandidates, response);

    return response.games[0] ?? null;
  }

  private selectCandidateProcesses(processes: string[]): CandidateProcess[] {
    const candidates = new Map<string, CandidateProcess>();

    for (const processName of processes.slice(0, MAX_PROCESS_NAMES_PER_REQUEST)) {
      const normalized = this.normalizeProcessName(processName);

      if (!normalized) {
        continue;
      }

      const cacheKey = this.normalizeCacheKey(normalized);
      const existing = candidates.get(cacheKey);
      const candidate = {
        processName,
        score: this.scoreCandidateProcess(processName, normalized)
      };

      if (!existing || candidate.score > existing.score) {
        candidates.set(cacheKey, candidate);
      }
    }

    return Array.from(candidates.values())
      .sort((left, right) => right.score - left.score || left.processName.localeCompare(right.processName));
  }

  private normalizeProcessName(value: string): string {
    const normalized = value.trim()
      .replace(/\.exe$/i, '')
      .replace(/[_-]+/g, ' ')
      .replace(/\s+/g, ' ')
      .trim();
    const cacheKey = this.normalizeCacheKey(normalized);

    if (normalized.length < 4 || normalized.length > 96 || this.shouldIgnoreProcessName(cacheKey)) {
      return '';
    }

    return normalized;
  }

  private shouldIgnoreProcessName(cacheKey: string): boolean {
    return IGNORED_PROCESS_NAMES.has(cacheKey)
      || IGNORED_PROCESS_PATTERNS.some((pattern) => pattern.test(cacheKey));
  }

  private scoreCandidateProcess(rawValue: string, normalized: string): number {
    let score = 0;

    if (/\.exe$/i.test(rawValue.trim())) {
      score += 12;
    }

    if (/[A-Z]/.test(normalized) && /[a-z]/.test(normalized)) {
      score += 4;
    }

    if (/\d/.test(normalized)) {
      score += 1;
    }

    if (normalized.length >= 5 && normalized.length <= 32) {
      score += 2;
    }

    if (normalized.includes(' ')) {
      score -= 2;
    }

    return score;
  }

  private findCachedGame(candidates: CandidateProcess[]): MatchedGame | null | undefined {
    if (candidates.length === 0) {
      return null;
    }

    let hasCachedMissForEveryCandidate = true;

    for (const candidate of candidates) {
      const cached = this.getCachedMatch(candidate.processName);

      if (cached === undefined) {
        hasCachedMissForEveryCandidate = false;
        continue;
      }

      if (cached) {
        return cached;
      }
    }

    return hasCachedMissForEveryCandidate ? null : undefined;
  }

  private storeMatchResponse(candidates: CandidateProcess[], response: GameMatchResponse): void {
    for (const game of response.games) {
      this.setCachedMatch(game.processName, game, POSITIVE_CACHE_TTL_MS);
    }

    if (response.rateLimited) {
      return;
    }

    const matchedProcessKeys = new Set(response.games.map((game) => this.normalizeCacheKey(game.processName)));

    for (const candidate of candidates) {
      if (!matchedProcessKeys.has(this.normalizeCacheKey(candidate.processName))) {
        this.setCachedMatch(candidate.processName, null, NEGATIVE_CACHE_TTL_MS);
      }
    }
  }

  private hasFreshCacheEntry(processName: string): boolean {
    return this.getCachedMatch(processName) !== undefined;
  }

  private getCachedMatch(processName: string): MatchedGame | null | undefined {
    const cache = this.readMatchCache();
    const cacheKey = this.normalizeCacheKey(processName);
    const cached = cache[cacheKey];

    if (!cached) {
      return undefined;
    }

    if (cached.expiresAt <= Date.now()) {
      this.writeMatchCache(Object.fromEntries(
        Object.entries(cache).filter(([key]) => key !== cacheKey)
      ));

      return undefined;
    }

    return cached.game;
  }

  private setCachedMatch(processName: string, game: MatchedGame | null, ttlMs: number): void {
    const cache = this.readMatchCache();

    cache[this.normalizeCacheKey(processName)] = {
      expiresAt: Date.now() + ttlMs,
      game
    };

    this.writeMatchCache(cache);
  }

  private readMatchCache(): Record<string, CachedGameMatch> {
    try {
      const parsed = JSON.parse(localStorage.getItem(GAME_MATCH_CACHE_STORAGE_KEY) ?? '{}') as unknown;

      return this.normalizeMatchCache(parsed);
    } catch {
      return {};
    }
  }

  private normalizeMatchCache(value: unknown): Record<string, CachedGameMatch> {
    if (!value || typeof value !== 'object' || Array.isArray(value)) {
      return {};
    }

    const cache: Record<string, CachedGameMatch> = {};

    for (const [key, entry] of Object.entries(value)) {
      if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
        continue;
      }

      const cached = entry as Partial<CachedGameMatch>;

      if (typeof cached.expiresAt === 'number') {
        cache[key] = {
          expiresAt: cached.expiresAt,
          game: this.normalizeCachedGame(cached.game)
        };
      }
    }

    return cache;
  }

  private normalizeCachedGame(value: unknown): MatchedGame | null {
    if (!value || typeof value !== 'object' || Array.isArray(value)) {
      return null;
    }

    const game = value as Partial<MatchedGame>;

    if (typeof game.id !== 'string' || typeof game.name !== 'string' || typeof game.processName !== 'string') {
      return null;
    }

    return {
      id: game.id,
      name: game.name,
      iconUrl: typeof game.iconUrl === 'string' ? game.iconUrl : undefined,
      store: this.normalizeGameStore(game.store),
      processName: game.processName
    };
  }

  private normalizeGameStore(value: unknown): GameStoreLink | undefined {
    if (!value || typeof value !== 'object' || Array.isArray(value)) {
      return undefined;
    }

    const store = value as Partial<GameStoreLink>;

    if (typeof store.name !== 'string' || typeof store.url !== 'string' || !this.isExternalUrl(store.url)) {
      return undefined;
    }

    return {
      id: typeof store.id === 'string' ? store.id : undefined,
      name: store.name,
      slug: typeof store.slug === 'string' ? store.slug : undefined,
      domain: typeof store.domain === 'string' ? store.domain : undefined,
      url: store.url
    };
  }

  private writeMatchCache(cache: Record<string, CachedGameMatch>): void {
    const entries = Object.entries(cache)
      .filter(([, entry]) => entry.expiresAt > Date.now())
      .sort((left, right) => right[1].expiresAt - left[1].expiresAt)
      .slice(0, MAX_LOCAL_CACHE_ENTRIES);

    localStorage.setItem(GAME_MATCH_CACHE_STORAGE_KEY, JSON.stringify(Object.fromEntries(entries)));
  }

  private normalizeCacheKey(value: string): string {
    return value.trim()
      .replace(/\.exe$/i, '')
      .replace(/[_-]+/g, ' ')
      .replace(/\s+/g, ' ')
      .toLowerCase();
  }

  private applyMatchedGame(game: MatchedGame | null): void {
    if (!game) {
      this.setCurrentActivity(null);
      return;
    }

    const previous = this.currentActivity;
    const activity: GameActivity = {
      id: game.id,
      name: game.name,
      iconUrl: game.iconUrl,
      store: game.store,
      startedAt: previous?.id === game.id ? previous.startedAt : Date.now()
    };

    this.setCurrentActivity(activity);
  }

  private setCurrentActivity(activity: GameActivity | null): void {
    if (this.isSameActivity(this.currentActivity, activity)) {
      return;
    }

    this.currentActivity = activity;

    const user = this.currentUser();

    if (user) {
      this.store.dispatch(UsersActions.updateGameActivity({
        userId: user.id,
        gameActivity: activity
      }));
    }

    this.webrtc.broadcastMessage({
      type: 'game-activity',
      oderId: user?.oderId || user?.id,
      displayName: user?.displayName || 'User',
      gameActivity: activity
    });
  }

  private handlePeerEvent(event: ChatEvent): void {
    if (event.type !== 'game-activity') {
      return;
    }

    const peerIdentifier = event.fromPeerId ?? event.oderId;

    if (!peerIdentifier) {
      return;
    }

    const currentUser = this.currentUser();

    if (peerIdentifier === currentUser?.id || peerIdentifier === currentUser?.oderId) {
      return;
    }

    const user = this.findUser(peerIdentifier);

    if (!user) {
      return;
    }

    this.store.dispatch(UsersActions.updateGameActivity({
      userId: user.id,
      gameActivity: this.normalizeIncomingActivity(event.gameActivity)
    }));
  }

  private sendCurrentActivityToPeer(peerId: string): void {
    const user = this.currentUser();

    if (!user) {
      return;
    }

    this.webrtc.sendToPeer(peerId, {
      type: 'game-activity',
      oderId: user.oderId || user.id,
      displayName: user.displayName || 'User',
      gameActivity: this.currentActivity
    });
  }

  private findUser(identifier: string): User | null {
    return this.allUsers().find((user) => user.id === identifier || user.oderId === identifier) ?? null;
  }

  private normalizeIncomingActivity(value: GameActivity | null | undefined): GameActivity | null {
    if (!value || typeof value.id !== 'string' || typeof value.name !== 'string' || typeof value.startedAt !== 'number') {
      return null;
    }

    return {
      id: value.id,
      name: value.name,
      iconUrl: typeof value.iconUrl === 'string' ? value.iconUrl : undefined,
      store: this.normalizeGameStore(value.store),
      startedAt: value.startedAt
    };
  }

  private isSameActivity(previous: GameActivity | null, next: GameActivity | null): boolean {
    return previous?.id === next?.id
      && previous?.name === next?.name
      && previous?.iconUrl === next?.iconUrl
      && previous?.store?.url === next?.store?.url
      && previous?.startedAt === next?.startedAt;
  }

  private isExternalUrl(value: string): boolean {
    return value.startsWith('http://') || value.startsWith('https://');
  }

  private buildProcessHash(processNames: string[]): string {
    return processNames.map((name) => name.trim().toLowerCase())
      .sort()
      .join('|');
  }

  private getScanIntervalMs(): number {
    const storedValue = Number.parseInt(localStorage.getItem(SCAN_INTERVAL_STORAGE_KEY) ?? '', 10);
    const interval = Number.isFinite(storedValue) ? storedValue : DEFAULT_SCAN_INTERVAL_MS;

    return Math.min(Math.max(interval, MIN_SCAN_INTERVAL_MS), MAX_SCAN_INTERVAL_MS);
  }
}
