import type { Database } from 'bun:sqlite';
import { ensureUserExists } from '../db';
import { getDb as getGlobalDb } from '../db/locator';
import { normalizeWhatsAppId } from '../utils/whatsapp';
import { Metrics } from './metrics';
import { IdentityService } from './identity';
import { AllowedGroups } from './allowed-groups';
import { toIsoSqlUTC } from '../utils/datetime';
import { publishGroupCoveragePrompt } from './onboarding';
import { fetchGroupsFromAPI as apiFetchGroups, fetchGroupMembersFromAPI as apiFetchMembers } from './group-sync/api';
import { upsertGroups as repoUpsertGroups } from './group-sync/repo';
import { cacheActiveGroups as computeActiveCache } from './group-sync/cache';
import { reconcileGroupMembers as reconcileMembers } from './group-sync/reconcile';
import { detectGroupChanges, type GroupRow } from './group-sync/changes';
import { handleDeactivatedGroups } from './group-sync/deactivation';
import {
  isSnapshotFresh as _isSnapshotFresh,
  isUserActiveInGroup as _isUserActiveInGroup,
  getActiveGroupIdsForUser as _getActiveGroupIdsForUser,
  getFreshMemberGroupsForUser as _getFreshMemberGroupsForUser,
} from './group-sync/membership';
import {
  resolveInterval,
  createSchedulerState,
  startScheduler,
  stopScheduler,
  secondsUntilNextTick,
  type SchedulerState,
} from './group-sync/scheduler';

type EvolutionGroup = {
  id: string;
  subject: string;
  linkedParent?: string;
};

// ---------------------------------------------------------------------------
// Helpers (file-private)
// ---------------------------------------------------------------------------

const activeQuery = `id, active, COALESCE(archived,0) AS archived, COALESCE(is_community,0) AS is_community, name`;

function loadGroupRows(db: Database): GroupRow[] {
  return db
    .prepare(`SELECT ${activeQuery} FROM groups`)
    .all() as GroupRow[];
}

function isCommunityFlag(g: any): boolean {
  return !!(
    g?.isCommunity ||
    g?.is_community ||
    g?.isCommunityAnnounce ||
    g?.is_community_announce
  );
}

function updateCacheAndMetrics(db: Database, cache: Map<string, string>): void {
  computeActiveCacheInto(db, cache);
  Metrics.set('active_groups', cache.size);
  const mRow = db
    .prepare(`SELECT COUNT(*) AS c FROM group_members WHERE is_active = 1`)
    .get() as { c?: number } | undefined;
  Metrics.set('active_members', Number(mRow?.c || 0));
}

function computeActiveCacheInto(
  db: Database,
  cache: Map<string, string>
): void {
  const map = computeActiveCache(db);
  cache.clear();
  for (const [id, name] of map) cache.set(id, name);
  console.log(`Cached ${cache.size} active groups`);
}

// ---------------------------------------------------------------------------
// GroupSyncService
// ---------------------------------------------------------------------------

export class GroupSyncService {
  // ---- DB instance injection ----
  private static _dbInstance: Database | null = null;
  static get dbInstance(): Database {
    return (this._dbInstance as Database) ?? getGlobalDb();
  }
  static set dbInstance(value: Database) {
    this._dbInstance = value;
  }

  // ---- In-memory cache ----
  public static readonly activeGroupsCache = new Map<string, string>();

  // ---- Scheduler state ----
  private static lastSyncAttempt = 0;
  private static _lastChangedActive: string[] = [];
  private static _groupsScheduler: SchedulerState = createSchedulerState();
  private static _membersScheduler: SchedulerState = createSchedulerState();

  // ===================================================================
  // Sync groups
  // ===================================================================

  static async syncGroups(
    force = false
  ): Promise<{ added: number; updated: number }> {
    if (!this.shouldSync(force)) return { added: 0, updated: 0 };

    const startedAt = Date.now();
    Metrics.inc('sync_runs_total');

    try {
      const groups = (await this.fetchGroupsFromAPI()) as unknown as EvolutionGroup[];
      console.log(
        'ℹ️ Sin filtrar por comunidad (modo multicomunidad). Total grupos:',
        groups.length
      );

      const before = loadGroupRows(this.dbInstance);
      const result = await repoUpsertGroups(this.dbInstance, groups as any);
      const after = loadGroupRows(this.dbInstance);

      // Detect changes
      const { newlyActivated, newlyDeactivated } =
        detectGroupChanges(before, after);
      this._lastChangedActive = newlyActivated;

      // Handle deactivated groups
      await handleDeactivatedGroups(this.dbInstance, newlyDeactivated);

      // Fill missing labels
      try {
        this.fillMissingAllowedGroupLabels(groups);
      } catch {}

      // Update cache & metrics
      updateCacheAndMetrics(this.dbInstance, this.activeGroupsCache);
      Metrics.set('last_sync_timestamp_seconds', Math.floor(Date.now() / 1000));
      Metrics.set('last_sync_ok', 1);
      Metrics.set('last_sync_duration_ms', Date.now() - startedAt);

      return result;
    } catch (error) {
      console.error('Group sync failed:', error);
      Metrics.inc('sync_errors_total');
      Metrics.set('last_sync_ok', 0);
      throw error;
    } finally {
      this.lastSyncAttempt = Date.now();
    }
  }

  private static shouldSync(force = false): boolean {
    if (force) return true;
    const elapsed = Date.now() - this.lastSyncAttempt;
    const interval = resolveInterval(
      'GROUP_SYNC_INTERVAL_MS',
      24 * 60 * 60 * 1000
    );
    if (elapsed <= interval) {
      console.debug(
        `Next sync available in ${Math.round((interval - elapsed) / 1000)} seconds`
      );
      return false;
    }
    return true;
  }

  // ===================================================================
  // Initial group check
  // ===================================================================

  static async checkInitialGroups(): Promise<void> {
    const count = this.getActiveGroupsCount();
    if (count > 0) {
      computeActiveCacheInto(this.dbInstance, this.activeGroupsCache);
      console.log(`✅ Using ${count} existing groups from database`);
      return;
    }

    const communityId = process.env.WHATSAPP_COMMUNITY_ID;
    if (!communityId) {
      await this.listAndExitWithCommunityInfo();
      return; // unreachable — listAndExitWithCommunityInfo calls process.exit
    }

    console.log('⚠️ No groups found in database - performing initial sync');
    try {
      const { added } = await this.syncGroups();
      if (added === 0) {
        throw new Error(
          'Initial group sync completed but no groups were added'
        );
      }
      computeActiveCacheInto(this.dbInstance, this.activeGroupsCache);
      console.log(`✅ Initial group sync completed - added ${added} groups`);
    } catch (error) {
      console.error(
        '❌ Critical: Initial group sync failed - no groups available'
      );
      console.error(
        error instanceof Error ? error.message : 'Unknown error'
      );
      process.exit(1);
    }
  }

  private static async listAndExitWithCommunityInfo(): Promise<void> {
    console.log(
      'ℹ️ WHATSAPP_COMMUNITY_ID no definido - mostrando comunidades disponibles'
    );
    try {
      const allGroups = (await this.fetchGroupsFromAPI()) as unknown as EvolutionGroup[];
      const communities = allGroups.filter(g => g.linkedParent);

      if (communities.length === 0) {
        console.log(
          '❌ No se encontraron comunidades (grupos con linkedParent)'
        );
      } else {
        console.log(
          '\n📋 Comunidades disponibles (copia el ID completo):'
        );
        console.log('='.repeat(80));
        console.log('Nombre'.padEnd(30), 'ID Comunidad');
        console.log('-'.repeat(30), '-'.repeat(48));
        for (const c of communities) {
          console.log(c.subject.padEnd(30), c.id);
        }
        console.log('='.repeat(80));
        console.log(
          '⚠️ ATENCIÓN: Estos IDs son sensibles. No los compartas públicamente.'
        );
        console.log(
          `\n⏳ El proceso terminará automáticamente en 120 segundos...`
        );

        const start = Date.now();
        await new Promise<void>(resolve => {
          const timer = setInterval(() => {
            const remaining = Math.ceil((120_000 - (Date.now() - start)) / 1000);
            process.stdout.write(`\r⏳ Tiempo restante: ${remaining}s   `);
          }, 1000);
          setTimeout(() => {
            clearInterval(timer);
            resolve();
          }, 120_000);
        });

        console.log(
          '\n\n✅ Listado completado. Por favor configura WHATSAPP_COMMUNITY_ID'
        );
      }
      process.exit(0);
    } catch (error) {
      console.error(
        '❌ Error al obtener comunidades:',
        error instanceof Error ? error.message : error
      );
      process.exit(1);
    }
  }

  private static getActiveGroupsCount(): number {
    const row = this.dbInstance
      .prepare(
        `SELECT COUNT(*) AS count
         FROM groups
         WHERE active = TRUE
           AND COALESCE(is_community,0) = 0
           AND COALESCE(archived,0) = 0`
      )
      .get() as { count?: number } | undefined;
    return Number(row?.count || 0);
  }

  // ===================================================================
  // In-memory cache helpers
  // ===================================================================

  static isGroupActive(groupId: string): boolean {
    return this.activeGroupsCache.has(groupId);
  }

  static refreshActiveGroupsCache(): void {
    computeActiveCacheInto(this.dbInstance, this.activeGroupsCache);
  }

  static getLastChangedActive(): string[] {
    return Array.from(this._lastChangedActive || []);
  }

  // ===================================================================
  // Label filling
  // ===================================================================

  private static fillMissingAllowedGroupLabels(
    allGroups: EvolutionGroup[]
  ): number {
    try {
      if (!Array.isArray(allGroups) || allGroups.length === 0) return 0;

      const nameById = new Map<string, string>();
      for (const g of allGroups) {
        if (isCommunityFlag(g) || !g?.id) continue;
        const name = String(g.subject || '').trim();
        if (name) nameById.set(String(g.id), name);
      }
      if (nameById.size === 0) return 0;

      const rows = this.dbInstance
        .prepare(
          `SELECT group_id AS id
           FROM allowed_groups
           WHERE label IS NULL OR TRIM(label) = ''`
        )
        .all() as Array<{ id: string }>;
      if (!rows || rows.length === 0) return 0;

      let filled = 0;
      for (const r of rows) {
        const id = r?.id ? String(r.id) : null;
        if (!id) continue;
        const label = nameById.get(id);
        if (label) {
          try {
            AllowedGroups.upsertPending(id, label, null);
          } catch {}
          filled++;
        }
      }
      if (filled > 0) {
        Metrics.inc('allowed_groups_labels_filled_total', filled);
        console.log(
          `ℹ️ Rellenadas ${filled} labels faltantes en allowed_groups`
        );
      }
      return filled;
    } catch (e) {
      console.warn(
        '⚠️ No se pudieron rellenar labels faltantes en allowed_groups:',
        e
      );
      return 0;
    }
  }

  // ===================================================================
  // Upsert helpers
  // ===================================================================

  private static async upsertGroups(
    groups: EvolutionGroup[]
  ): Promise<{ added: number; updated: number }> {
    return repoUpsertGroups(this.dbInstance, groups as any);
  }

  static upsertMemberSeen(
    groupId: string,
    userId: string,
    nowIso?: string
  ): void {
    if (!groupId || !userId) return;
    const now = nowIso || toIsoSqlUTC(new Date());
    try {
      ensureUserExists(userId, this.dbInstance);
    } catch {}
    this.dbInstance
      .prepare(
        `INSERT INTO group_members (group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at)
         VALUES (?, ?, 0, 1, ?, ?)
         ON CONFLICT(group_id, user_id) DO UPDATE SET
           is_active = 1,
           last_seen_at = excluded.last_seen_at`
      )
      .run(groupId, userId, now, now);
  }

  // ===================================================================
  // Member reconciliation
  // ===================================================================

  static reconcileGroupMembers(
    groupId: string,
    snapshot: Array<{ userId: string; isAdmin: boolean }>,
    nowIso?: string
  ): { added: number; updated: number; deactivated: number } {
    const res = reconcileMembers(
      this.dbInstance,
      groupId,
      snapshot,
      nowIso || toIsoSqlUTC(new Date())
    );
    try {
      this.computeAndPublishAliasCoverage(groupId);
    } catch {}
    return res;
  }

  private static computeAndPublishAliasCoverage(groupId: string): void {
    try {
      const rows = this.dbInstance
        .prepare(
          `SELECT user_id FROM group_members WHERE group_id = ? AND is_active = 1`
        )
        .all(groupId) as Array<{ user_id: string }>;

      const total = rows.length;
      if (total === 0) {
        Metrics.set('alias_coverage_ratio', 1, { group_id: groupId });
        return;
      }

      let resolvable = 0;
      for (const r of rows) {
        const uid = String(r.user_id || '');
        if (/^\d+$/.test(uid)) {
          resolvable++;
          continue;
        }
        try {
          const resolved = IdentityService.resolveAliasOrNull(uid);
          if (resolved && /^\d+$/.test(resolved)) resolvable++;
        } catch {}
      }
      const ratio = Math.max(
        0,
        Math.min(1, total > 0 ? resolvable / total : 1)
      );
      Metrics.set('alias_coverage_ratio', ratio, { group_id: groupId });

      try {
        publishGroupCoveragePrompt(this.dbInstance, groupId, ratio);
      } catch {}
    } catch (e) {
      console.warn(
        '⚠️ No se pudo calcular alias_coverage_ratio para',
        groupId,
        e
      );
    }
  }

  // ===================================================================
  // API fetch wrappers (kept as class methods for test stubbing)
  // ===================================================================

  private static async fetchGroupsFromAPI(): Promise<EvolutionGroup[]> {
    return (await apiFetchGroups()) as unknown as EvolutionGroup[];
  }

  private static cacheActiveGroups(): void {
    computeActiveCacheInto(this.dbInstance, this.activeGroupsCache);
  }

  // ===================================================================
  // Members sync loop
  // ===================================================================

  private static async fetchGroupMembersFromAPI(
    groupId: string
  ): Promise<Array<{ userId: string; isAdmin: boolean }>> {
    return apiFetchMembers(groupId);
  }

  private static async _syncMembersLoop(
    ids: Iterable<string>,
    label: string
  ): Promise<{
    groups: number;
    added: number;
    updated: number;
    deactivated: number;
  }> {
    if (process.env.NODE_ENV === 'test') {
      return { groups: 0, added: 0, updated: 0, deactivated: 0 };
    }

    const enforce =
      String(process.env.GROUP_GATING_MODE || 'off').toLowerCase() ===
      'enforce';

    let groups = 0,
      added = 0,
      updated = 0,
      deactivated = 0;

    for (const groupId of ids) {
      try {
        if (enforce && !this.isGroupAllowedSafe(groupId)) continue;
        const snapshot = await this.fetchGroupMembersFromAPI(groupId);
        const res = this.reconcileGroupMembers(groupId, snapshot);
        groups++;
        added += res.added;
        updated += res.updated;
        deactivated += res.deactivated;
      } catch (e) {
        console.error(
          `❌ Failed to sync members for group ${groupId}:`,
          e instanceof Error ? e.message : String(e)
        );
      }
    }
    console.log(`ℹ️ ${label}:`, { groups, added, updated, deactivated });
    return { groups, added, updated, deactivated };
  }

  private static isGroupAllowedSafe(groupId: string): boolean {
    try {
      if (!AllowedGroups.isAllowed(groupId)) {
        Metrics.inc('sync_skipped_group_total');
        return false;
      }
    } catch {}
    return true;
  }

  static async syncMembersForActiveGroups(): Promise<{
    groups: number;
    added: number;
    updated: number;
    deactivated: number;
  }> {
    if (process.env.NODE_ENV === 'test') {
      return { groups: 0, added: 0, updated: 0, deactivated: 0 };
    }
    if (this.activeGroupsCache.size === 0) {
      computeActiveCacheInto(this.dbInstance, this.activeGroupsCache);
    }
    return this._syncMembersLoop(
      Array.from(this.activeGroupsCache.keys()),
      'Members sync summary'
    );
  }

  static async syncMembersForGroups(
    ids: string[]
  ): Promise<{
    groups: number;
    added: number;
    updated: number;
    deactivated: number;
  }> {
    if (process.env.NODE_ENV === 'test') {
      return { groups: 0, added: 0, updated: 0, deactivated: 0 };
    }
    if (!Array.isArray(ids) || ids.length === 0) {
      return { groups: 0, added: 0, updated: 0, deactivated: 0 };
    }
    return this._syncMembersLoop(ids, 'Targeted members sync summary');
  }

  // ===================================================================
  // Schedulers
  // ===================================================================

  static startGroupsScheduler(): void {
    if (process.env.NODE_ENV === 'test') return;
    const interval = resolveInterval(
      'GROUP_SYNC_INTERVAL_MS',
      24 * 60 * 60 * 1000
    );
    startScheduler(
      this._groupsScheduler,
      interval,
      async () => { await this.syncGroups(); },
      'Groups'
    );
  }

  static stopGroupsScheduler(): void {
    stopScheduler(this._groupsScheduler);
  }

  static getSecondsUntilNextGroupSync(
    nowMs: number = Date.now()
  ): number | null {
    return secondsUntilNextTick(this._groupsScheduler, nowMs);
  }

  static startMembersScheduler(): void {
    if (process.env.NODE_ENV === 'test') return;
    const interval = resolveInterval(
      'GROUP_MEMBERS_SYNC_INTERVAL_MS',
      6 * 60 * 60 * 1000
    );
    startScheduler(
      this._membersScheduler,
      interval,
      async () => { await this.syncMembersForActiveGroups(); },
      'Members'
    );
  }

  static stopMembersScheduler(): void {
    stopScheduler(this._membersScheduler);
  }

  // ===================================================================
  // Membership queries (thin wrappers)
  // ===================================================================

  static isSnapshotFresh(
    groupId: string,
    nowMs: number = Date.now()
  ): boolean {
    return _isSnapshotFresh(this.dbInstance, groupId, nowMs);
  }

  static isUserActiveInGroup(userId: string, groupId: string): boolean {
    return _isUserActiveInGroup(this.dbInstance, userId, groupId);
  }

  static getActiveGroupIdsForUser(userId: string): string[] {
    return _getActiveGroupIdsForUser(this.dbInstance, userId);
  }

  static getFreshMemberGroupsForUser(userId: string): string[] {
    return _getFreshMemberGroupsForUser(this.dbInstance, userId);
  }

  // ===================================================================
  // Group existence & label resolution
  // ===================================================================

  static ensureGroupExists(
    groupId: string,
    name?: string | null
  ): { created: boolean; reactivated: boolean; updatedName: boolean } {
    if (!groupId)
      return { created: false, reactivated: false, updatedName: false };

    let created = false,
      reactivated = false,
      updatedName = false;

    this.dbInstance.transaction(() => {
      const row = this.dbInstance
        .prepare(`SELECT id, active, name FROM groups WHERE id = ?`)
        .get(groupId) as any;

      if (!row) {
        const community = process.env.WHATSAPP_COMMUNITY_ID || '';
        this.dbInstance
          .prepare(
            `INSERT INTO groups (id, community_id, name, active, last_verified)
             VALUES (?, ?, ?, 1, CURRENT_TIMESTAMP)`
          )
          .run(groupId, community, name || null);
        created = true;
      } else {
        const hasNewName =
          typeof name === 'string' &&
          name.trim().length > 0 &&
          name !== row.name;
        if (row.active !== 1 || hasNewName) {
          this.dbInstance
            .prepare(
              `UPDATE groups
               SET active = 1,
                   name = COALESCE(?, name),
                   last_verified = CURRENT_TIMESTAMP
               WHERE id = ?`
            )
            .run(hasNewName ? name : null, groupId);
          reactivated = row.active !== 1;
          updatedName = hasNewName;
        }
      }
    })();

    updateCacheAndMetrics(this.dbInstance, this.activeGroupsCache);
    return { created, reactivated, updatedName };
  }

  /**
   * Resolve a group's display name through a 3-tier fallback:
   *   1. In-memory cache
   *   2. Database (groups.name)
   *   3. Evolution API (only outside test mode)
   * Side-effect: persists the resolved label to both groups and
   *              allowed_groups at whichever tier succeeds.
   */
  static async ensureGroupLabelAndName(
    groupId: string
  ): Promise<string | null> {
    if (!groupId) return null;

    // Tier 1 — memory cache
    const cached = this.activeGroupsCache.get(groupId);
    if (cached?.trim()) {
      this.persistLabelAndRefresh(groupId, cached);
      return cached;
    }

    // Tier 2 — DB
    try {
      const row = this.dbInstance
        .prepare('SELECT name FROM groups WHERE id = ?')
        .get(groupId) as { name?: string | null } | undefined;
      const dbName = row?.name ? String(row.name).trim() : '';
      if (dbName) {
        this.persistLabelAndRefresh(groupId, dbName);
        return dbName;
      }
    } catch {}

    // Tier 3 — API (skip in tests)
    if (process.env.NODE_ENV !== 'test') {
      try {
        const groups = (await this.fetchGroupsFromAPI()) as unknown as EvolutionGroup[];
        const g = groups.find(gg => gg?.id === groupId);
        const subject = g?.subject ? String(g.subject).trim() : '';
        if (subject) {
          this.persistLabelAndRefresh(groupId, subject);
          return subject;
        }
      } catch {}
    }

    return null;
  }

  private static persistLabelAndRefresh(
    groupId: string,
    label: string
  ): void {
    try {
      this.ensureGroupExists(groupId, label);
    } catch {}
    try {
      AllowedGroups.upsertPending(groupId, label, null);
    } catch {}
    computeActiveCacheInto(this.dbInstance, this.activeGroupsCache);
  }

  // ===================================================================
  // Sync members for a single group
  // ===================================================================

  static async syncMembersForGroup(
    groupId: string
  ): Promise<{ added: number; updated: number; deactivated: number }> {
    if (
      String(process.env.GROUP_GATING_MODE || 'off').toLowerCase() ===
        'enforce' &&
      !this.isGroupAllowedSafe(groupId)
    ) {
      return { added: 0, updated: 0, deactivated: 0 };
    }

    try {
      this.ensureGroupExists(groupId);
      const snapshot = await this.fetchGroupMembersFromAPI(groupId);
      return this.reconcileGroupMembers(groupId, snapshot);
    } catch (e) {
      console.error(
        `❌ Failed to sync members for group ${groupId}:`,
        e instanceof Error ? e.message : String(e)
      );
      return { added: 0, updated: 0, deactivated: 0 };
    }
  }
}
