import { FastifyInstance } from 'fastify';
import { createClient, RedisClientType } from 'redis';
import { randomUUID } from 'node:crypto';
import { DbService } from '../services/db.service.js';
import { SessionDirector } from '../services/session-director.service.js';
import { Candidate } from '../services/generators.service.js';

interface ActivePlan {
  sessionId: string;
  plan: Candidate[];
  seedTrackId: string | null;
}

const PLAN_TTL_SEC = 2 * 3600;

function planKey(userId: string): string {
  return `v2:plan:${userId}`;
}

export default async function v2Routes(fastify: FastifyInstance, options: { dbService: DbService; sessionDirector: SessionDirector }) {
  const { dbService, sessionDirector: director } = options;

  const redisClient: RedisClientType = createClient({
    url: process.env.REDIS_URL || 'redis://localhost:6379',
  });
  await redisClient.connect();
  fastify.addHook('onClose', async () => { await redisClient.quit(); });

  async function getActivePlan(userId: string): Promise<ActivePlan | null> {
    const raw = await redisClient.get(planKey(userId));
    if (!raw) return null;
    return JSON.parse(raw) as ActivePlan;
  }

  async function setActivePlan(userId: string, plan: ActivePlan): Promise<void> {
    await redisClient.setEx(planKey(userId), PLAN_TTL_SEC, JSON.stringify(plan));
  }

  async function delActivePlan(userId: string): Promise<void> {
    await redisClient.del(planKey(userId));
  }

  // ponytail: per-user SET NX PX lock around the getActivePlan->mutate->setActivePlan
  // span, released via a CAS Lua script so a slow holder can't delete a lock a later
  // request already owns. Ceiling: this serializes /next and /feedback per-user only
  // (fine — plans aren't shared across users); if plan mutation logic ever needs to
  // span multiple keys/services atomically, swap this for a WATCH/MULTI transaction
  // or move the plan into a single Lua script instead of an app-level lock.
  const RELEASE_LOCK_LUA = `if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end`;

  async function withPlanLock<T>(userId: string, fn: () => Promise<T>): Promise<T> {
    const lockKey = `v2:planlock:${userId}`;
    const token = randomUUID();
    const deadline = Date.now() + 5000;
    let acquired = false;
    while (Date.now() < deadline) {
      const res = await redisClient.set(lockKey, token, { NX: true, PX: 3000 });
      if (res) { acquired = true; break; }
      await new Promise((r) => setTimeout(r, 20 + Math.random() * 30));
    }
    if (!acquired) {
      throw new Error('Timed out waiting for active-plan lock');
    }
    try {
      return await fn();
    } finally {
      await redisClient.eval(RELEASE_LOCK_LUA, { keys: [lockKey], arguments: [token] });
    }
  }

  /**
   * POST /api/v2/vibe/start — start a v2 session
   * Body: { seedTrackId? }
   * Returns: { sessionId, plan: Candidate[] }
   */
  fastify.post('/v2/vibe/start', async (request, reply) => {
    const userId = (request.headers['x-user-id'] as string) || '00000000-0000-0000-0000-000000000000';
    const { seedTrackId } = request.body as { seedTrackId?: string };

    const sessionId = await dbService.createSessionState(userId, undefined, { energy: 0.5, novelty_hunger: 0.3 });
    const plan = await director.buildPlan(userId, sessionId, seedTrackId);

    await setActivePlan(userId, { sessionId, plan, seedTrackId: seedTrackId ?? null });
    return reply.send({ sessionId, plan: plan.slice(0, 10) });
  });

  /**
   * GET /api/v2/vibe/next — get next track from the plan
   * Returns: { track, planRemaining }
   */
  fastify.get('/v2/vibe/next', async (request, reply) => {
    const userId = (request.headers['x-user-id'] as string) || '00000000-0000-0000-0000-000000000000';

    const result = await withPlanLock(userId, async () => {
      const active = await getActivePlan(userId);

      if (!active || active.plan.length === 0) {
        return null;
      }

      const next = active.plan.shift()!;
      // Enrich with track details
      const track = await dbService.getTrackById(next.trackId);

      // Replan if running low
      if (active.plan.length < 5) {
        const refill = await director.replan(userId, active.sessionId, active.plan, [next.trackId], active.seedTrackId ?? undefined);
        active.plan = refill;
      }

      await setActivePlan(userId, active);

      return { track, explanation: next.explanation, planRemaining: active.plan.length };
    });

    if (!result) {
      return reply.code(404).send({ error: 'No active plan. POST /api/v2/vibe/start first.' });
    }

    return reply.send(result);
  });

  /**
   * POST /api/v2/vibe/feedback — feedback that triggers replan
   * Body: { trackId, action: 'completed' | 'skipped' | 'promoted' | 'disliked' }
   */
  fastify.post('/v2/vibe/feedback', async (request, reply) => {
    const userId = (request.headers['x-user-id'] as string) || '00000000-0000-0000-0000-000000000000';
    const { trackId, action } = request.body as { trackId: string; action: string };

    if (!trackId || !action) {
      return reply.code(400).send({ error: 'trackId and action are required' });
    }

    // Route to existing handlers for evidence wiring
    if (action === 'completed') {
      await dbService.recordPlay(userId, trackId, true);
    } else if (action === 'skipped') {
      await dbService.recordSkip(userId, trackId);
    } else if (action === 'promoted') {
      await dbService.addFavorite(userId, trackId);
      await dbService.recordFeedback(userId, trackId, 'promoted');
    } else if (action === 'disliked') {
      await dbService.dislikeTrack(userId, trackId);
    }

    // Replan the session
    const planRemaining = await withPlanLock(userId, async () => {
      const active = await getActivePlan(userId);
      if (!active) return 0;
      const playedTrackIds = [trackId];
      const refill = await director.replan(userId, active.sessionId, active.plan, playedTrackIds, active.seedTrackId ?? undefined);
      active.plan = refill;
      await setActivePlan(userId, active);
      return active.plan.length;
    });

    return reply.send({ status: 'ok', planRemaining });
  });

  /**
   * GET /api/v2/vibe/plan — current plan for debugging
   */
  fastify.get('/v2/vibe/plan', async (request, reply) => {
    const userId = (request.headers['x-user-id'] as string) || '00000000-0000-0000-0000-000000000000';
    const active = await getActivePlan(userId);
    if (!active) return reply.send({ plan: [] });
    return reply.send({ sessionId: active.sessionId, planRemaining: active.plan.length, plan: active.plan });
  });

  /**
   * GET /api/v2/state — current listener state (debugging)
   */
  fastify.get('/v2/state', async (request, reply) => {
    const userId = (request.headers['x-user-id'] as string) || '00000000-0000-0000-0000-000000000000';
    const state = await director.buildState(userId);
    const fatigue = await director.computeFatigue(userId);
    const budgets = await director.getBudgets(userId);
    return reply.send({ state, fatigue: Object.fromEntries(
      Object.entries(fatigue).map(([k, v]) => [k, v instanceof Map ? Object.fromEntries(v) : v])
    ), budgets });
  });
}
