/*
 * Priority Forge - Cross-project task prioritization
 * Copyright (C) 2026 Priority Forge Contributors
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

import { Router, Request, Response } from 'express';
import { storage } from '../storage/jsonStorage';
import {
  TaskAlreadyExistsError,
  VersionConflictError,
  TaskAlreadyClaimedError,
  TaskNotClaimableError,
} from '../storage/errors';
import { syncEmitter } from '../sync';
import { withIdempotencyKey } from '../utils/idempotency';
import { CreateTaskDTO, UpdateTaskDTO, Priority } from '../types/schema';

/**
 * CONC-003: parse an optional version precondition from either the
 * `If-Match` header (preferred, REST idiom — "5" quoted or 5 plain) or
 * the `expectedVersion` body field. Returns `undefined` if neither is
 * present, triggering an unconditional update.
 */
function parseExpectedVersion(req: Request, body: Record<string, unknown>): number | undefined {
  const header = req.get('If-Match');
  if (header) {
    const stripped = header.trim().replace(/^"|"$/g, ''); // strip quotes if present
    const n = Number(stripped);
    if (Number.isFinite(n) && n > 0) return n;
  }
  const fromBody = body.expectedVersion;
  if (typeof fromBody === 'number' && Number.isFinite(fromBody) && fromBody > 0) {
    return fromBody;
  }
  return undefined;
}

const router = Router();

// GET /tasks - List active tasks (sorted by priority score in V2)
// Use ?all=true to include completed tasks
router.get('/', async (req: Request, res: Response) => {
  try {
    const includeCompleted = req.query.all === 'true';
    const tasks = await storage.getTasks(includeCompleted);
    res.json(tasks);
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch tasks' });
  }
});

// GET /tasks/completed - List completed tasks only
router.get('/completed', async (_req: Request, res: Response) => {
  try {
    const tasks = await storage.getCompletedTasks();
    res.json(tasks);
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch completed tasks' });
  }
});

// GET /tasks/priority/:level - Filter by priority
router.get('/priority/:level', async (req: Request, res: Response) => {
  try {
    const level = req.params.level.toUpperCase() as Priority;
    if (!['P0', 'P1', 'P2', 'P3'].includes(level)) {
      return res.status(400).json({ error: 'Invalid priority level. Use P0, P1, P2, or P3' });
    }
    const tasks = await storage.getTasksByPriority(level);
    res.json(tasks);
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch tasks' });
  }
});

// GET /tasks/project/:projectId - Filter by project
router.get('/project/:projectId', async (req: Request, res: Response) => {
  try {
    const tasks = await storage.getTasksByProject(req.params.projectId);
    res.json(tasks);
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch tasks' });
  }
});

// GET /tasks/top - V2: Get top priority task
router.get('/top', async (_req: Request, res: Response) => {
  try {
    const task = await storage.getTopPriority();
    if (!task) {
      return res.json({ message: 'No tasks in queue' });
    }
    res.json(task);
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch top priority task' });
  }
});

// GET /tasks/:id - Get task by ID
router.get('/:id', async (req: Request, res: Response) => {
  try {
    const task = await storage.getTask(req.params.id);
    if (!task) {
      return res.status(404).json({ error: 'Task not found' });
    }
    res.json(task);
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch task' });
  }
});

// POST /tasks - Create task
//
// CONC-007: If the request includes an `Idempotency-Key` header, repeated
// POSTs with the same key return the same task (within a 24h TTL) instead
// of creating duplicates. Standard HTTP idempotency pattern — clients use
// this on retries after timeouts where the original response may or may
// not have been received.
router.post('/', async (req: Request, res: Response) => {
  try {
    const data: CreateTaskDTO = req.body;
    if (!data.priority || !data.task || !data.project) {
      return res.status(400).json({ error: 'priority, task, and project are required' });
    }
    if (!['P0', 'P1', 'P2', 'P3'].includes(data.priority)) {
      return res.status(400).json({ error: 'Invalid priority. Use P0, P1, P2, or P3' });
    }

    const idempotencyKey = req.get('Idempotency-Key');
    const task = idempotencyKey
      ? await withIdempotencyKey(
          idempotencyKey,
          () => storage.createTask(data),
          (taskId) => storage.getTask(taskId),
        )
      : await storage.createTask(data);

    if (task) syncEmitter.emitTaskCreated(task as unknown as Record<string, unknown>);
    res.status(201).json(task);
  } catch (error) {
    // CONC-002: distinguish duplicate-id errors (409) from generic failures (500)
    if (error instanceof TaskAlreadyExistsError) {
      return res.status(error.status).json({
        error: error.message,
        code: error.code,
      });
    }
    console.error('[POST /tasks] unexpected error:', error);
    res.status(500).json({ error: 'Failed to create task' });
  }
});

// PUT /tasks/:id - Update task
router.put('/:id', async (req: Request, res: Response) => {
  try {
    // CONC-003: strip expectedVersion from body so it's not mis-merged into
    // the task record; extract optimistic-concurrency precondition from
    // either `If-Match` header or `expectedVersion` body field.
    const { expectedVersion: _v, ...rest } = req.body as Record<string, unknown>;
    const data: UpdateTaskDTO = rest as UpdateTaskDTO;
    const expectedVersion = parseExpectedVersion(req, req.body || {});

    if (data.priority && !['P0', 'P1', 'P2', 'P3'].includes(data.priority)) {
      return res.status(400).json({ error: 'Invalid priority. Use P0, P1, P2, or P3' });
    }
    const previous = (await storage.getTask(req.params.id)) ?? null;
    const task = await storage.updateTask(req.params.id, data, expectedVersion);
    if (!task) {
      return res.status(404).json({ error: 'Task not found' });
    }
    syncEmitter.emitTaskUpdated(
      req.params.id,
      data as unknown as Record<string, unknown>,
      (previous ?? {}) as unknown as Record<string, unknown>,
    );
    // status → in_progress is the same "selection" signal MCP fires from
    // update_task; mirror that here so the team learns who's working on what.
    if (data.status === 'in_progress') {
      syncEmitter.emitTaskSelected(task as unknown as Record<string, unknown>);
    }
    // CONC-003: ETag header so clients can use If-Match on subsequent updates
    if (task.version !== undefined) {
      res.set('ETag', `"${task.version}"`);
    }
    res.json(task);
  } catch (error) {
    // CONC-003: 412 Precondition Failed on version conflict; include current
    // version in body so the client can re-read + retry without another GET.
    if (error instanceof VersionConflictError) {
      return res.status(error.status).json({
        error: error.message,
        code: error.code,
        currentVersion: error.currentVersion,
        expectedVersion: error.expectedVersion,
      });
    }
    console.error('[PUT /tasks/:id] unexpected error:', error);
    res.status(500).json({ error: 'Failed to update task' });
  }
});

// CONC-004: POST /tasks/:id/claim - Atomically claim a task for an agent
// Body: { agentId: string }
// Responses:
//   200 { task } — claim granted (or idempotent re-claim by same agent)
//   400 — missing agentId
//   404 — task not found
//   409 TASK_NOT_CLAIMABLE — task is in terminal state
//   409 TASK_ALREADY_CLAIMED — different agent holds the claim
router.post('/:id/claim', async (req: Request, res: Response) => {
  try {
    const agentId = typeof req.body?.agentId === 'string' ? req.body.agentId.trim() : '';
    if (!agentId) {
      return res.status(400).json({ error: 'agentId (string, non-empty) is required in request body' });
    }
    const task = await storage.claimTask(req.params.id, agentId);
    if (!task) {
      return res.status(404).json({ error: 'Task not found' });
    }
    syncEmitter.emitTaskSelected(task as unknown as Record<string, unknown>);
    if (task.version !== undefined) {
      res.set('ETag', `"${task.version}"`);
    }
    res.json(task);
  } catch (error) {
    if (error instanceof TaskAlreadyClaimedError) {
      return res.status(error.status).json({
        error: error.message,
        code: error.code,
        claimedBy: error.claimedBy,
      });
    }
    if (error instanceof TaskNotClaimableError) {
      return res.status(error.status).json({
        error: error.message,
        code: error.code,
        currentStatus: error.currentStatus,
      });
    }
    console.error('[POST /tasks/:id/claim] unexpected error:', error);
    res.status(500).json({ error: 'Failed to claim task' });
  }
});

// DELETE /tasks/:id - Delete task
router.delete('/:id', async (req: Request, res: Response) => {
  try {
    const deleted = await storage.deleteTask(req.params.id);
    if (!deleted) {
      return res.status(404).json({ error: 'Task not found' });
    }
    res.status(204).send();
  } catch (error) {
    res.status(500).json({ error: 'Failed to delete task' });
  }
});

// POST /tasks/:id/context-switch - Log context switch (V3 prep)
router.post('/:id/context-switch', async (req: Request, res: Response) => {
  try {
    const task = await storage.getTask(req.params.id);
    if (!task) {
      return res.status(404).json({ error: 'Task not found' });
    }
    await storage.logContextSwitch(req.params.id);
    syncEmitter.emitContextSwitch(req.params.id, task.task, task.project);
    res.json({ message: 'Context switch logged', taskId: req.params.id });
  } catch (error) {
    res.status(500).json({ error: 'Failed to log context switch' });
  }
});

// POST /tasks/:id/complete - Complete task with outcome (V3 prep)
router.post('/:id/complete', async (req: Request, res: Response) => {
  try {
    const { outcome } = req.body;
    if (!outcome || !['completed', 'cancelled', 'deferred'].includes(outcome)) {
      return res.status(400).json({ error: 'outcome must be completed, cancelled, or deferred' });
    }
    const taskBefore = await storage.getTask(req.params.id);
    const record = await storage.completeTask(req.params.id, outcome);
    if (!record) {
      return res.status(404).json({ error: 'Task not found' });
    }
    if (taskBefore) {
      syncEmitter.emitTaskCompleted(
        taskBefore as unknown as Record<string, unknown>,
        outcome as string,
      );
    }
    res.json(record);
  } catch (error) {
    res.status(500).json({ error: 'Failed to complete task' });
  }
});

export default router;
