import type { Database } from 'bun:sqlite';
import { db, ensureUserExists } from '../db';
import { AllowedGroups } from '../services/allowed-groups';
import { isGroupId } from '../utils/whatsapp';
import { ResponseQueue } from '../services/response-queue';
import { Metrics } from '../services/metrics';

type CreateTaskInput = {
  description: string;
  due_date?: string | null; // Expect 'YYYY-MM-DD' or null
  group_id?: string | null; // Full JID (e.g., 'xxx@g.us') or null
  created_by: string;       // Normalized user ID
};

type CreateAssignmentInput = {
  user_id: string;    // Normalized user ID
  assigned_by: string; // Normalized user ID (typically created_by)
};

export class TaskService {
  static dbInstance: Database = db;

  static createTask(task: CreateTaskInput, assignments: CreateAssignmentInput[] = []): number {
    const MAX_DISPLAY_CODE = 9999;

    const runTx = this.dbInstance.transaction(() => {
      const pickNextDisplayCode = (): number => {
        const rows = this.dbInstance
          .prepare(`
            SELECT display_code
            FROM tasks
            WHERE display_code IS NOT NULL
              AND (
                COALESCE(completed, 0) = 0
                OR (completed_at IS NOT NULL AND completed_at >= strftime('%Y-%m-%d %H:%M:%f','now','-24 hours'))
              )
            ORDER BY display_code ASC
          `)
          .all() as Array<{ display_code: number }>;
        let expect = 1;
        for (const r of rows) {
          const dc = Number(r.display_code || 0);
          if (dc < expect) continue;
          if (dc === expect) {
            expect++;
            if (expect > MAX_DISPLAY_CODE) break;
            continue;
          }
          // encontrado hueco
          break;
        }
        if (expect > MAX_DISPLAY_CODE) {
          throw new Error('No hay códigos disponibles (límite alcanzado)');
        }
        return expect;
      };

      const insertTask = this.dbInstance.prepare(`
        INSERT INTO tasks (description, due_date, group_id, created_by, display_code)
        VALUES (?, ?, ?, ?, ?)
      `);

      const ensuredCreator = ensureUserExists(task.created_by, this.dbInstance);
      if (!ensuredCreator) {
        throw new Error('No se pudo asegurar created_by');
      }

      // Si el group_id no existe en la tabla groups, usar NULL para no violar la FK
      let groupIdToInsert = task.group_id ?? null;

      // Etapa 5: en modo 'enforce', si es un grupo no permitido, forzar a NULL (compatibilidad)
      try {
        const mode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase();
        if (groupIdToInsert && isGroupId(groupIdToInsert) && mode === 'enforce') {
          try { (AllowedGroups as any).dbInstance = this.dbInstance; } catch {}
          if (!AllowedGroups.isAllowed(groupIdToInsert)) {
            groupIdToInsert = null;
          }
        }
      } catch {}

      if (groupIdToInsert) {
        const exists = this.dbInstance.prepare(`SELECT 1 FROM groups WHERE id = ? AND COALESCE(is_community,0) = 0`).get(groupIdToInsert);
        if (!exists) {
          groupIdToInsert = null;
        }
      }

      // Elegir display_code global reutilizable entre tareas activas
      const displayCode = pickNextDisplayCode();

      const runResult = insertTask.run(
        task.description,
        task.due_date ?? null,
        groupIdToInsert,
        ensuredCreator,
        displayCode
      );
      const taskId = Number((runResult as any).lastInsertRowid);

      if (assignments.length > 0) {
        const insertAssignment = this.dbInstance.prepare(`
          INSERT INTO task_assignments (task_id, user_id, assigned_by)
          VALUES (?, ?, ?)
        `);

        // Evitar duplicados por (task_id, user_id) tras asegurar usuarios
        const seen = new Set<string>();
        for (const a of assignments) {
          const ensuredUser = ensureUserExists(a.user_id, this.dbInstance);
          if (!ensuredUser) continue;
          if (seen.has(ensuredUser)) continue;
          seen.add(ensuredUser);

          const ensuredAssigner =
            ensureUserExists(a.assigned_by || ensuredCreator, this.dbInstance) || ensuredCreator;

          insertAssignment.run(taskId, ensuredUser, ensuredAssigner);
        }
      }

      return taskId;
    });

    return runTx();
  }

  // Listar pendientes del grupo (limite por defecto 10)
  static listGroupPending(groupId: string, limit: number = 10): Array<{
    id: number;
    description: string;
    due_date: string | null;
    group_id: string | null;
    display_code: number | null;
    assignees: string[];
  }> {
    const rows = this.dbInstance
      .prepare(`
        SELECT id, description, due_date, group_id, display_code
        FROM tasks
        WHERE group_id = ?
          AND COALESCE(completed, 0) = 0 AND completed_at IS NULL
        ORDER BY 
          CASE WHEN due_date IS NULL THEN 1 ELSE 0 END,
          due_date ASC,
          id ASC
        LIMIT ?
      `)
      .all(groupId, limit) as any[];

    const getAssignees = this.dbInstance.prepare(`
      SELECT user_id FROM task_assignments
      WHERE task_id = ?
      ORDER BY assigned_at ASC
    `);

    return rows.map((r) => {
      const assigneesRows = getAssignees.all(r.id) as any[];
      const assignees = assigneesRows.map((a) => String(a.user_id));
      return {
        id: Number(r.id),
        description: String(r.description || ''),
        due_date: r.due_date ? String(r.due_date) : null,
        group_id: r.group_id ? String(r.group_id) : null,
        display_code: r.display_code != null ? Number(r.display_code) : null,
        assignees,
      };
    });
  }

  // Listar pendientes asignadas al usuario (limite por defecto 10)
  static listUserPending(userId: string, limit: number = 10): Array<{
    id: number;
    description: string;
    due_date: string | null;
    group_id: string | null;
    display_code: number | null;
    assignees: string[];
  }> {
    const rows = this.dbInstance
      .prepare(`
        SELECT t.id, t.description, t.due_date, t.group_id, t.display_code
        FROM tasks t
        INNER JOIN task_assignments a ON a.task_id = t.id
        WHERE a.user_id = ?
          AND COALESCE(t.completed, 0) = 0 AND t.completed_at IS NULL
        ORDER BY 
          CASE WHEN t.due_date IS NULL THEN 1 ELSE 0 END,
          t.due_date ASC,
          t.id ASC
        LIMIT ?
      `)
      .all(userId, limit) as any[];

    const getAssignees = this.dbInstance.prepare(`
      SELECT user_id FROM task_assignments
      WHERE task_id = ?
      ORDER BY assigned_at ASC
    `);

    return rows.map((r) => {
      const assigneesRows = getAssignees.all(r.id) as any[];
      const assignees = assigneesRows.map((a) => String(a.user_id));
      return {
        id: Number(r.id),
        description: String(r.description || ''),
        due_date: r.due_date ? String(r.due_date) : null,
        group_id: r.group_id ? String(r.group_id) : null,
        display_code: r.display_code != null ? Number(r.display_code) : null,
        assignees,
      };
    });
  }

  // Contar pendientes del grupo (sin límite)
  static countGroupPending(groupId: string): number {
    const row = this.dbInstance
      .prepare(`
        SELECT COUNT(*) as cnt
        FROM tasks
        WHERE group_id = ?
          AND COALESCE(completed, 0) = 0 AND completed_at IS NULL
      `)
      .get(groupId) as any;
    return Number(row?.cnt || 0);
  }

  // Contar pendientes asignadas al usuario (sin límite)
  static countUserPending(userId: string): number {
    const row = this.dbInstance
      .prepare(`
        SELECT COUNT(*) as cnt
        FROM tasks t
        INNER JOIN task_assignments a ON a.task_id = t.id
        WHERE a.user_id = ?
          AND COALESCE(t.completed, 0) = 0 AND t.completed_at IS NULL
      `)
      .get(userId) as any;
    return Number(row?.cnt || 0);
  }

  // Completar tarea: registra quién completó e idempotente
  static completeTask(taskId: number, completedBy: string): {
    status: 'updated' | 'already' | 'not_found';
    task?: { id: number; description: string; due_date: string | null; display_code: number | null };
  } {
    const ensured = ensureUserExists(completedBy, this.dbInstance);

    const existing = this.dbInstance
      .prepare(`
        SELECT id, description, due_date, completed, completed_at, display_code, group_id
        FROM tasks
        WHERE id = ?
      `)
      .get(taskId) as any;

    if (!existing) {
      return { status: 'not_found' };
    }

    if (existing.completed || existing.completed_at) {
      return {
        status: 'already',
        task: {
          id: Number(existing.id),
          description: String(existing.description || ''),
          due_date: existing.due_date ? String(existing.due_date) : null,
          display_code: existing.display_code != null ? Number(existing.display_code) : null,
        },
      };
    }

    this.dbInstance
      .prepare(`
        UPDATE tasks
        SET completed = 1,
            completed_at = strftime('%Y-%m-%d %H:%M:%f', 'now'),
            completed_by = ?
        WHERE id = ?
      `)
      .run(ensured, taskId);

    // Fase 2: reacción ✅ al completar dentro del TTL y con gating
    try {
      const rxEnabled = String(process.env.REACTIONS_ENABLED || 'false').toLowerCase();
      const enabled = ['true','1','yes','on'].includes(rxEnabled);
      if (enabled) {
        let origin: any = null;
        try {
          origin = this.dbInstance.prepare(`
          SELECT chat_id, message_id, created_at, participant, from_me
          FROM task_origins
          WHERE task_id = ?
        `).get(taskId) as any;
        } catch {
          origin = this.dbInstance.prepare(`
          SELECT chat_id, message_id, created_at
          FROM task_origins
          WHERE task_id = ?
        `).get(taskId) as any;
        }

        if (origin && origin.chat_id && origin.message_id) {
          const chatId = String(origin.chat_id);
          const scope = String(process.env.REACTIONS_SCOPE || 'groups').toLowerCase();
          if (scope === 'all' || isGroupId(chatId)) {
            // TTL desde REACTIONS_TTL_DAYS (usar tal cual; default 14 si inválido)
            const ttlDaysEnv = Number(process.env.REACTIONS_TTL_DAYS);
            const ttlDays = Number.isFinite(ttlDaysEnv) && ttlDaysEnv > 0 ? ttlDaysEnv : 14;
            const maxAgeMs = ttlDays * 24 * 60 * 60 * 1000;

            const createdRaw = String(origin.created_at || '');
            const createdIso = createdRaw.includes('T') ? createdRaw : (createdRaw.replace(' ', 'T') + 'Z');
            const createdMs = Date.parse(createdIso);
            const withinTtl = Number.isFinite(createdMs) ? (Date.now() - createdMs <= maxAgeMs) : false;

            if (withinTtl) {
              // Gating 'enforce' para grupos
              let allowed = true;
              if (isGroupId(chatId)) {
                try { (AllowedGroups as any).dbInstance = this.dbInstance; } catch {}
                const mode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase();
                if (mode === 'enforce') {
                  try { allowed = AllowedGroups.isAllowed(chatId); } catch { allowed = true; }
                }
              }
              if (allowed) {
                // Encolar reacción ✅ con idempotencia; no bloquear si falla
                const participant = origin && origin.participant ? String(origin.participant) : undefined;
                const fromMe = (origin && (origin.from_me === 1 || origin.from_me === true)) ? true : undefined;
                ResponseQueue.enqueueReaction(chatId, String(origin.message_id), '✅', { participant, fromMe })
                  .catch(() => {});
              }
            }
          }
        }
      }
    } catch {}

    return {
      status: 'updated',
      task: {
        id: Number(existing.id),
        description: String(existing.description || ''),
        due_date: existing.due_date ? String(existing.due_date) : null,
        display_code: existing.display_code != null ? Number(existing.display_code) : null,
      },
    };
  }

  // Listar pendientes sin dueño del grupo (limite por defecto 10)
  static listGroupUnassigned(groupId: string, limit: number = 10): Array<{
    id: number;
    description: string;
    due_date: string | null;
    group_id: string | null;
    display_code: number | null;
    assignees: string[];
  }> {
    const rows = this.dbInstance
      .prepare(`
        SELECT id, description, due_date, group_id, display_code
        FROM tasks
        WHERE group_id = ?
          AND COALESCE(completed, 0) = 0 AND completed_at IS NULL
          AND NOT EXISTS (
            SELECT 1 FROM task_assignments ta WHERE ta.task_id = tasks.id
          )
        ORDER BY 
          CASE WHEN due_date IS NULL THEN 1 ELSE 0 END,
          due_date ASC,
          id ASC
        LIMIT ?
      `)
      .all(groupId, limit) as any[];

    return rows.map((r) => ({
      id: Number(r.id),
      description: String(r.description || ''),
      due_date: r.due_date ? String(r.due_date) : null,
      group_id: r.group_id ? String(r.group_id) : null,
      display_code: r.display_code != null ? Number(r.display_code) : null,
      assignees: [],
    }));
  }

  // Contar pendientes sin dueño del grupo (sin límite)
  static countGroupUnassigned(groupId: string): number {
    const row = this.dbInstance
      .prepare(`
        SELECT COUNT(*) as cnt
        FROM tasks t
        WHERE t.group_id = ?
          AND COALESCE(t.completed, 0) = 0 AND t.completed_at IS NULL
          AND NOT EXISTS (
            SELECT 1 FROM task_assignments a WHERE a.task_id = t.id
          )
      `)
      .get(groupId) as any;
    return Number(row?.cnt || 0);
  }

  // Tomar tarea (claim): idempotente
  static claimTask(taskId: number, userId: string): {
    status: 'claimed' | 'already' | 'not_found' | 'completed';
    task?: { id: number; description: string; due_date: string | null; display_code: number | null };
  } {
    const ensuredUser = ensureUserExists(userId, this.dbInstance);
    if (!ensuredUser) {
      throw new Error('No se pudo asegurar el usuario');
    }

    const existing = this.dbInstance
      .prepare(`
        SELECT id, description, due_date, completed, completed_at, display_code
        FROM tasks
        WHERE id = ?
      `)
      .get(taskId) as any;

    if (!existing) {
      return { status: 'not_found' };
    }

    if (existing.completed || existing.completed_at) {
      return {
        status: 'completed',
        task: {
          id: Number(existing.id),
          description: String(existing.description || ''),
          due_date: existing.due_date ? String(existing.due_date) : null,
          display_code: existing.display_code != null ? Number(existing.display_code) : null,
        },
      };
    }

    const already = this.dbInstance
      .prepare(`SELECT 1 FROM task_assignments WHERE task_id = ? AND user_id = ?`)
      .get(taskId, ensuredUser);

    if (already) {
      return {
        status: 'already',
        task: {
          id: Number(existing.id),
          description: String(existing.description || ''),
          due_date: existing.due_date ? String(existing.due_date) : null,
          display_code: existing.display_code != null ? Number(existing.display_code) : null,
        },
      };
    }

    const insertAssignment = this.dbInstance.prepare(`
      INSERT OR IGNORE INTO task_assignments (task_id, user_id, assigned_by)
      VALUES (?, ?, ?)
    `);

    this.dbInstance.transaction(() => {
      insertAssignment.run(taskId, ensuredUser, ensuredUser);
    })();

    return {
      status: 'claimed',
      task: {
        id: Number(existing.id),
        description: String(existing.description || ''),
        due_date: existing.due_date ? String(existing.due_date) : null,
        display_code: existing.display_code != null ? Number(existing.display_code) : null,
      },
    };
  }

  // Soltar tarea (unassign): idempotente
  static unassignTask(taskId: number, userId: string): {
    status: 'unassigned' | 'not_assigned' | 'not_found' | 'completed' | 'forbidden_personal';
    task?: { id: number; description: string; due_date: string | null; display_code: number | null };
    now_unassigned?: boolean; // true si tras soltar no quedan asignados
  } {
    const ensuredUser = ensureUserExists(userId, this.dbInstance);
    if (!ensuredUser) {
      throw new Error('No se pudo asegurar el usuario');
    }

    const existing = this.dbInstance
      .prepare(`
        SELECT id, description, due_date, completed, completed_at, display_code
        FROM tasks
        WHERE id = ?
      `)
      .get(taskId) as any;

    if (!existing) {
      return { status: 'not_found' };
    }

    if (existing.completed || existing.completed_at) {
      return {
        status: 'completed',
        task: {
          id: Number(existing.id),
          description: String(existing.description || ''),
          due_date: existing.due_date ? String(existing.due_date) : null,
          display_code: existing.display_code != null ? Number(existing.display_code) : null,
        },
      };
    }

    // Regla: no permitir soltar si es tarea personal y el usuario es el único asignatario
    try {
      const stats = this.dbInstance.prepare(`
        SELECT COUNT(*) AS cnt,
               SUM(CASE WHEN user_id = ? THEN 1 ELSE 0 END) AS mine
        FROM task_assignments
        WHERE task_id = ?
      `).get(ensuredUser, taskId) as any;
      const cnt = Number(stats?.cnt || 0);
      const mine = Number(stats?.mine || 0) > 0;
      if ((existing.group_id == null || existing.group_id === null) && cnt === 1 && mine) {
        return {
          status: 'forbidden_personal',
          task: {
            id: Number(existing.id),
            description: String(existing.description || ''),
            due_date: existing.due_date ? String(existing.due_date) : null,
            display_code: existing.display_code != null ? Number(existing.display_code) : null,
          },
          now_unassigned: false,
        };
      }
    } catch {}

    const deleteStmt = this.dbInstance.prepare(`
      DELETE FROM task_assignments
      WHERE task_id = ? AND user_id = ?
    `);

    const result = deleteStmt.run(taskId, ensuredUser) as any;

    const cntRow = this.dbInstance
      .prepare(`SELECT COUNT(*) as cnt FROM task_assignments WHERE task_id = ?`)
      .get(taskId) as any;
    const remaining = Number(cntRow?.cnt || 0);

    if (result.changes && result.changes > 0) {
      return {
        status: 'unassigned',
        task: {
          id: Number(existing.id),
          description: String(existing.description || ''),
          due_date: existing.due_date ? String(existing.due_date) : null,
          display_code: existing.display_code != null ? Number(existing.display_code) : null,
        },
        now_unassigned: remaining === 0,
      };
    }

    return {
      status: 'not_assigned',
      task: {
        id: Number(existing.id),
        description: String(existing.description || ''),
        due_date: existing.due_date ? String(existing.due_date) : null,
        display_code: existing.display_code != null ? Number(existing.display_code) : null,
      },
      now_unassigned: remaining === 0,
    };
  }

  // ===== Helpers adicionales para consumidores (Etapa 3) =====

  // Devuelve datos básicos de una tarea, o null si no existe
  static getTaskById(taskId: number): {
    id: number;
    description: string;
    due_date: string | null;
    group_id: string | null;
    display_code: number | null;
    completed: number;
    completed_at: string | null;
  } | null {
    const row = this.dbInstance.prepare(`
      SELECT 
        id,
        description,
        due_date,
        group_id,
        display_code,
        COALESCE(completed, 0) as completed,
        completed_at
      FROM tasks
      WHERE id = ?
    `).get(taskId) as any;
    if (!row) return null;
    return {
      id: Number(row.id),
      description: String(row.description || ''),
      due_date: row.due_date ? String(row.due_date) : null,
      group_id: row.group_id ? String(row.group_id) : null,
      display_code: row.display_code != null ? Number(row.display_code) : null,
      completed: Number(row.completed || 0),
      completed_at: row.completed_at ? String(row.completed_at) : null,
    };
  }

  // Buscar tarea activa por display_code global
  static getActiveTaskByDisplayCode(displayCode: number): { id: number; description: string; due_date: string | null; display_code: number | null } | null {
    const row = this.dbInstance.prepare(`
      SELECT id, description, due_date, display_code
      FROM tasks
      WHERE display_code = ? AND COALESCE(completed, 0) = 0 AND completed_at IS NULL
      LIMIT 1
    `).get(displayCode) as any;
    if (!row) return null;
    return {
      id: Number(row.id),
      description: String(row.description || ''),
      due_date: row.due_date ? String(row.due_date) : null,
      display_code: row.display_code != null ? Number(row.display_code) : null,
    };
  }

  // Lista tareas sin responsable para múltiples grupos.
  // Implementación simple: reutiliza el método existente por grupo.
  static listUnassignedByGroups(groupIds: string[], limitPerGroup: number = 10): Map<string, Array<{
    id: number;
    description: string;
    due_date: string | null;
    group_id: string | null;
    assignees: string[];
  }>> {
    const out = new Map<string, Array<{
      id: number;
      description: string;
      due_date: string | null;
      group_id: string | null;
      assignees: string[];
    }>>();
    if (!Array.isArray(groupIds) || groupIds.length === 0) return out;
    for (const gid of groupIds) {
      const rows = this.listGroupUnassigned(gid, limitPerGroup);
      if (rows.length > 0) {
        out.set(gid, rows);
      }
    }
    return out;
  }

  // Listar todas las tareas activas en todos los grupos (ordenadas por due_date ASC, NULL al final)
  static listAllActive(limit: number = 50): Array<{
    id: number;
    description: string;
    due_date: string | null;
    group_id: string | null;
    group_name: string | null;
    display_code: number | null;
  }> {
    const rows = this.dbInstance
      .prepare(`
        SELECT t.id, t.description, t.due_date, t.group_id, t.display_code, g.name AS group_name
        FROM tasks t
        LEFT JOIN groups g ON g.id = t.group_id
        WHERE COALESCE(t.completed, 0) = 0 AND t.completed_at IS NULL
          AND (t.group_id IS NULL OR EXISTS (
            SELECT 1 FROM groups g2
            WHERE g2.id = t.group_id
              AND COALESCE(g2.active,1)=1
              AND COALESCE(g2.archived,0)=0
              AND COALESCE(g2.is_community,0)=0
          ))
        ORDER BY 
          CASE WHEN t.due_date IS NULL THEN 1 ELSE 0 END,
          t.due_date ASC,
          t.id ASC
        LIMIT ?
      `)
      .all(limit) as any[];

    return rows.map(r => ({
      id: Number(r.id),
      description: String(r.description || ''),
      due_date: r.due_date ? String(r.due_date) : null,
      group_id: r.group_id ? String(r.group_id) : null,
      group_name: r.group_name ? String(r.group_name) : null,
      display_code: r.display_code != null ? Number(r.display_code) : null,
    }));
  }

  static countAllActive(): number {
    const row = this.dbInstance
      .prepare(`
        SELECT COUNT(*) AS cnt
        FROM tasks t
        WHERE COALESCE(t.completed, 0) = 0 AND t.completed_at IS NULL
          AND (t.group_id IS NULL OR EXISTS (
            SELECT 1 FROM groups g2
            WHERE g2.id = t.group_id
              AND COALESCE(g2.active,1)=1
              AND COALESCE(g2.archived,0)=0
              AND COALESCE(g2.is_community,0)=0
          ))
      `)
      .get() as any;
    return Number(row?.cnt || 0);
  }
}
