import { z } from 'zod';
import type { FastifyInstance } from 'fastify';
import { BadRequestError, ForbiddenError, NotFoundError } from '../../lib/errors.js';
import { executeAgent, cancelAgentRun, getActiveAgentRuns, onStepEvent } from './executor.js';
import { listTools, validateToolBindings } from './tool-registry.js';
import type { RunDoc } from '../runs/types.js';
import * as runRepo from '../runs/repository.js';

const ExecuteAgentSchema = z.object({
  agentId: z.string().min(1),
  input: z.record(z.unknown()).default({}),
  parentRunId: z.string().optional(),
});

function requireAuth(req: { jwtPayload?: { sub?: string; role?: string; productId?: string } }): {
  userId: string;
  productId: string;
} {
  const payload = req.jwtPayload;
  if (!payload?.sub) throw new ForbiddenError('Authentication required');
  return {
    userId: payload.sub,
    productId: payload.productId ?? process.env.DEFAULT_PRODUCT_ID ?? 'lysnrai',
  };
}

function requireAdmin(req: { jwtPayload?: { sub?: string; role?: string; productId?: string } }): {
  userId: string;
  productId: string;
} {
  const access = requireAuth(req);
  const role = req.jwtPayload?.role;
  if (!role || !['super_admin', 'admin'].includes(role)) {
    throw new ForbiddenError('Admin access required');
  }
  return access;
}

export async function agentExecutorRoutes(app: FastifyInstance) {
  // ── Execute agent ─────────────────────────────────────────

  app.post('/agents/execute', async req => {
    const access = requireAuth(req);
    const parsed = ExecuteAgentSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }

    return executeAgent({
      agentId: parsed.data.agentId,
      productId: access.productId,
      userId: access.userId,
      input: parsed.data.input,
      parentRunId: parsed.data.parentRunId,
      triggeredBy: access.userId,
    });
  });

  // ── Cancel run ────────────────────────────────────────────

  app.post('/runs/:id/cancel', async req => {
    const access = requireAuth(req);
    const { id } = req.params as { id: string };

    // Try in-memory cancel first (active agent runs)
    if (cancelAgentRun(id)) {
      return { cancelled: true, runId: id };
    }

    // Fall back to DB update for non-active runs
    try {
      await runRepo.updateRun(id, access.productId, {
        status: 'cancelled',
        completedAt: new Date().toISOString(),
        error: 'Cancelled by user',
      });
      return { cancelled: true, runId: id };
    } catch {
      throw new NotFoundError(`Run '${id}' not found or already completed`);
    }
  });

  // ── Active agent runs ─────────────────────────────────────

  app.get('/agents/active-runs', async req => {
    requireAdmin(req);
    return { activeRuns: getActiveAgentRuns() };
  });

  // ── Parent-child run DAG ──────────────────────────────────

  app.get('/runs/:id/children', async req => {
    const access = requireAuth(req);
    const { id } = req.params as { id: string };
    return runRepo.listChildRuns(access.productId, id);
  });

  app.get('/runs/:id/tree', async req => {
    const access = requireAuth(req);
    const { id } = req.params as { id: string };

    // Build a tree: get root run + all descendants
    const rootRun = await runRepo.getRun(id, access.productId);
    const children = await runRepo.listChildRuns(access.productId, id);
    const steps = await runRepo.listRunSteps(access.productId, id);

    return {
      run: rootRun,
      steps,
      children: await Promise.all(
        children.map(async (child: RunDoc) => ({
          run: child,
          steps: await runRepo.listRunSteps(access.productId, child.id),
        }))
      ),
    };
  });

  // ── Agent metrics ─────────────────────────────────────────

  app.get('/agents/:id/metrics', async req => {
    const access = requireAdmin(req);
    const { id } = req.params as { id: string };
    const query = req.query as { since?: string; limit?: string };

    const runs = await runRepo.listRuns(access.productId, {
      kind: 'agent',
      limit: Math.min(parseInt(query.limit ?? '100', 10), 500),
    });

    // Filter by agentId (stored in metadata)
    const agentRuns = runs.filter(
      r => r.metadata && (r.metadata as Record<string, unknown>).agentId === id
    );

    // Filter by since if provided
    const filteredRuns = query.since
      ? agentRuns.filter(r => r.createdAt >= query.since!)
      : agentRuns;

    const succeeded = filteredRuns.filter(r => r.status === 'succeeded');
    const failed = filteredRuns.filter(r => r.status === 'failed');
    const cancelled = filteredRuns.filter(r => r.status === 'cancelled');

    // Compute token usage from output metadata
    let totalTokens = 0;
    let totalPromptTokens = 0;
    let totalCompletionTokens = 0;
    for (const run of succeeded) {
      const output = run.output as Record<string, unknown> | undefined;
      if (output?.tokenUsage) {
        const usage = output.tokenUsage as { prompt?: number; completion?: number; total?: number };
        totalPromptTokens += usage.prompt ?? 0;
        totalCompletionTokens += usage.completion ?? 0;
        totalTokens += usage.total ?? 0;
      }
    }

    // Compute average duration
    const durations = succeeded
      .map(r => {
        const out = r.output as Record<string, unknown> | undefined;
        return (out?.durationMs as number | undefined) ?? 0;
      })
      .filter(d => d > 0);

    const avgDurationMs =
      durations.length > 0
        ? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length)
        : 0;

    return {
      agentId: id,
      totalRuns: filteredRuns.length,
      succeeded: succeeded.length,
      failed: failed.length,
      cancelled: cancelled.length,
      successRate:
        filteredRuns.length > 0 ? Math.round((succeeded.length / filteredRuns.length) * 100) : 0,
      avgDurationMs,
      tokenUsage: {
        prompt: totalPromptTokens,
        completion: totalCompletionTokens,
        total: totalTokens,
      },
    };
  });

  // ── Run step streaming (SSE) ──────────────────────────────

  app.get('/runs/:id/stream', async (req, reply) => {
    requireAuth(req);
    const { id } = req.params as { id: string };

    reply.raw.writeHead(200, {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive',
    });

    const unsubscribe = onStepEvent(id, event => {
      reply.raw.write(`data: ${JSON.stringify(event)}\n\n`);
    });

    // Send initial keepalive
    reply.raw.write(`data: ${JSON.stringify({ type: 'connected', runId: id })}\n\n`);

    req.raw.on('close', () => {
      unsubscribe();
    });

    // Don't end the response — keep it open for SSE
    await new Promise(() => {});
  });

  // ── Tool registry ─────────────────────────────────────────

  app.get('/tools', async req => {
    requireAdmin(req);
    return listTools();
  });

  app.post('/tools/validate', async req => {
    requireAdmin(req);
    const body = req.body as { bindings?: string[] };
    if (!body.bindings || !Array.isArray(body.bindings)) {
      throw new BadRequestError('bindings array is required');
    }
    return validateToolBindings(body.bindings);
  });

  // ── Agent scheduling ──────────────────────────────────────

  app.post('/agents/:id/schedule', async req => {
    const access = requireAdmin(req);
    const { id } = req.params as { id: string };
    const body = req.body as {
      cronExpression?: string;
      input?: Record<string, unknown>;
      enabled?: boolean;
    };

    if (!body.cronExpression) {
      throw new BadRequestError('cronExpression is required');
    }

    // Register as a job definition
    const { ensureJobDefinitions } = await import('../jobs/runner.js');
    const { registerJob } = await import('../jobs/registry.js');

    const jobName = `agent:${id}`;
    registerJob(jobName, async ctx => {
      const result = await executeAgent({
        agentId: id,
        productId: access.productId,
        userId: ctx.productId, // system-triggered
        input: body.input ?? {},
        triggeredBy: 'scheduler',
      });
      return {
        success: result.status === 'succeeded',
        message: result.status,
        metrics: {
          tokenUsage: result.tokenUsage,
          durationMs: result.durationMs,
        },
      };
    });

    await ensureJobDefinitions([
      {
        name: jobName,
        cron: body.cronExpression,
        description: `Scheduled execution of agent ${id}`,
      },
    ]);

    return { scheduled: true, jobName, cronExpression: body.cronExpression };
  });
}
