/**
 * Errand Router
 *
 * All 9 errand procedures: create, list, get, diff, complete, merge, delete, sendMessage, abandon.
 * Errands are small isolated changes that spawn a dedicated agent in a git worktree.
 */

import { z } from 'zod';
import { TRPCError } from '@trpc/server';
import { nanoid } from 'nanoid';
import { router } from '../trpc.js';
import type { ProcedureBuilder } from '../trpc.js';
import {
  requireErrandRepository,
  requireProjectRepository,
  requireAgentManager,
  requireBranchManager,
} from './_helpers.js';
import { writeErrandManifest } from '../../agent/file-io.js';
import { buildErrandPrompt } from '../../agent/prompts/index.js';
import { SimpleGitWorktreeManager } from '../../git/manager.js';
import { ensureProjectClone } from '../../git/project-clones.js';
import type { TRPCContext } from '../context.js';

// ErrandStatus values for input validation
const ErrandStatusValues = ['active', 'pending_review', 'conflict', 'merged', 'abandoned'] as const;

/**
 * Resolve the project's local clone path.
 * Throws INTERNAL_SERVER_ERROR if workspaceRoot is not available.
 */
async function resolveClonePath(
  project: { id: string; name: string; url: string },
  ctx: TRPCContext,
): Promise<string> {
  if (!ctx.workspaceRoot) {
    throw new TRPCError({
      code: 'INTERNAL_SERVER_ERROR',
      message: 'Workspace root not configured',
    });
  }
  return ensureProjectClone(project, ctx.workspaceRoot);
}

export function errandProcedures(publicProcedure: ProcedureBuilder) {
  return {
    errand: router({
      // -----------------------------------------------------------------------
      // errand.create
      // -----------------------------------------------------------------------
      create: publicProcedure
        .input(z.object({
          description: z.string(),
          projectId: z.string().min(1),
          baseBranch: z.string().optional(),
        }))
        .mutation(async ({ ctx, input }) => {
          // 1. Validate description length
          if (input.description.length > 200) {
            throw new TRPCError({
              code: 'BAD_REQUEST',
              message: `description must be ≤200 characters (${input.description.length} given)`,
            });
          }

          // 2. Look up project
          const project = await requireProjectRepository(ctx).findById(input.projectId);
          if (!project) {
            throw new TRPCError({ code: 'NOT_FOUND', message: 'Project not found' });
          }

          // 3. Generate slug
          let slug = input.description
            .toLowerCase()
            .replace(/\s+/g, '-')
            .replace(/[^a-z0-9-]/g, '')
            .slice(0, 50);
          if (!slug) slug = 'errand';

          // 4–5. Compute branch name with unique suffix
          const branchName = `cw/errand/${slug}-${nanoid().slice(0, 8)}`;

          // 6. Resolve base branch
          const baseBranch = input.baseBranch ?? 'main';

          // 7. Get project clone path and create branch
          const clonePath = await resolveClonePath(project, ctx);
          const branchManager = requireBranchManager(ctx);

          try {
            await branchManager.ensureBranch(clonePath, branchName, baseBranch);
          } catch (err) {
            throw new TRPCError({
              code: 'INTERNAL_SERVER_ERROR',
              message: err instanceof Error ? err.message : String(err),
            });
          }

          // 7.5. Create DB record early (agentId null) to get a stable ID for the worktree
          const repo = requireErrandRepository(ctx);
          let errand;
          try {
            errand = await repo.create({
              description: input.description,
              branch: branchName,
              baseBranch,
              agentId: null,
              projectId: input.projectId,
              status: 'active',
            });
          } catch (err) {
            try { await branchManager.deleteBranch(clonePath, branchName); } catch { /* no-op */ }
            throw new TRPCError({
              code: 'INTERNAL_SERVER_ERROR',
              message: err instanceof Error ? err.message : String(err),
            });
          }

          const errandId = errand.id;

          // 8. Create worktree using the DB-assigned errand ID
          const worktreeManager = new SimpleGitWorktreeManager(clonePath);
          let worktree;
          try {
            worktree = await worktreeManager.create(errandId, branchName, baseBranch);
          } catch (err) {
            // Clean up DB record and branch on worktree failure
            try { await repo.delete(errandId); } catch { /* no-op */ }
            try { await branchManager.deleteBranch(clonePath, branchName); } catch { /* no-op */ }
            throw new TRPCError({
              code: 'INTERNAL_SERVER_ERROR',
              message: err instanceof Error ? err.message : String(err),
            });
          }

          // 9. Build prompt
          const prompt = buildErrandPrompt(input.description);

          // 10. Spawn agent
          const agentManager = requireAgentManager(ctx);
          let agent;
          try {
            agent = await agentManager.spawn({
              prompt,
              mode: 'errand',
              cwd: worktree.path,
              provider: undefined,
            });
          } catch (err) {
            // Clean up worktree, DB record, and branch on spawn failure
            try { await worktreeManager.remove(errandId); } catch { /* no-op */ }
            try { await repo.delete(errandId); } catch { /* no-op */ }
            try { await branchManager.deleteBranch(clonePath, branchName); } catch { /* no-op */ }
            throw new TRPCError({
              code: 'INTERNAL_SERVER_ERROR',
              message: err instanceof Error ? err.message : String(err),
            });
          }

          // 11. Write errand manifest files
          await writeErrandManifest({
            agentWorkdir: worktree.path,
            errandId,
            description: input.description,
            branch: branchName,
            projectName: project.name,
            agentId: agent.id,
            agentName: agent.name,
          });

          // 12. Update DB record with agent ID
          await repo.update(errandId, { agentId: agent.id });

          // 13. Return result
          return { id: errandId, branch: branchName, agentId: agent.id };
        }),

      // -----------------------------------------------------------------------
      // errand.list
      // -----------------------------------------------------------------------
      list: publicProcedure
        .input(z.object({
          projectId: z.string().optional(),
          status: z.enum(ErrandStatusValues).optional(),
        }))
        .query(async ({ ctx, input }) => {
          return requireErrandRepository(ctx).findAll({
            projectId: input.projectId,
            status: input.status,
          });
        }),

      // -----------------------------------------------------------------------
      // errand.get
      // -----------------------------------------------------------------------
      get: publicProcedure
        .input(z.object({ id: z.string().min(1) }))
        .query(async ({ ctx, input }) => {
          const errand = await requireErrandRepository(ctx).findById(input.id);
          if (!errand) {
            throw new TRPCError({ code: 'NOT_FOUND', message: 'Errand not found' });
          }
          return {
            ...errand,
            conflictFiles: errand.conflictFiles ? (JSON.parse(errand.conflictFiles) as string[]) : null,
          };
        }),

      // -----------------------------------------------------------------------
      // errand.diff
      // -----------------------------------------------------------------------
      diff: publicProcedure
        .input(z.object({ id: z.string().min(1) }))
        .query(async ({ ctx, input }) => {
          const errand = await requireErrandRepository(ctx).findById(input.id);
          if (!errand) {
            throw new TRPCError({ code: 'NOT_FOUND', message: 'Errand not found' });
          }

          const project = await requireProjectRepository(ctx).findById(errand.projectId);
          if (!project) {
            throw new TRPCError({ code: 'NOT_FOUND', message: 'Project not found' });
          }

          const clonePath = await resolveClonePath(project, ctx);
          const diff = await requireBranchManager(ctx).diffBranches(
            clonePath,
            errand.baseBranch,
            errand.branch,
          );
          return { diff };
        }),

      // -----------------------------------------------------------------------
      // errand.complete
      // -----------------------------------------------------------------------
      complete: publicProcedure
        .input(z.object({ id: z.string().min(1) }))
        .mutation(async ({ ctx, input }) => {
          const repo = requireErrandRepository(ctx);
          const errand = await repo.findById(input.id);
          if (!errand) {
            throw new TRPCError({ code: 'NOT_FOUND', message: 'Errand not found' });
          }

          if (errand.status !== 'active') {
            throw new TRPCError({
              code: 'BAD_REQUEST',
              message: `Cannot complete an errand with status '${errand.status}'`,
            });
          }

          // Stop agent if present
          if (errand.agentId) {
            try {
              await requireAgentManager(ctx).stop(errand.agentId);
            } catch { /* no-op if already stopped */ }
          }

          const updated = await repo.update(input.id, { status: 'pending_review' });
          return updated;
        }),

      // -----------------------------------------------------------------------
      // errand.merge
      // -----------------------------------------------------------------------
      merge: publicProcedure
        .input(z.object({
          id: z.string().min(1),
          target: z.string().optional(),
        }))
        .mutation(async ({ ctx, input }) => {
          const repo = requireErrandRepository(ctx);
          const errand = await repo.findById(input.id);
          if (!errand) {
            throw new TRPCError({ code: 'NOT_FOUND', message: 'Errand not found' });
          }

          if (errand.status !== 'pending_review' && errand.status !== 'conflict') {
            throw new TRPCError({
              code: 'BAD_REQUEST',
              message: `Cannot merge an errand with status '${errand.status}'`,
            });
          }

          const targetBranch = input.target ?? errand.baseBranch;

          const project = await requireProjectRepository(ctx).findById(errand.projectId);
          if (!project) {
            throw new TRPCError({ code: 'NOT_FOUND', message: 'Project not found' });
          }

          const clonePath = await resolveClonePath(project, ctx);
          const result = await requireBranchManager(ctx).mergeBranch(
            clonePath,
            errand.branch,
            targetBranch,
          );

          if (result.success) {
            // Clean merge — remove worktree and mark merged
            const worktreeManager = new SimpleGitWorktreeManager(clonePath);
            try { await worktreeManager.remove(errand.id); } catch { /* no-op */ }
            await repo.update(input.id, { status: 'merged', conflictFiles: null });
            return { status: 'merged' };
          } else {
            // Conflict — persist conflict files and throw
            const conflictFilesList = result.conflicts ?? [];
            await repo.update(input.id, {
              status: 'conflict',
              conflictFiles: JSON.stringify(conflictFilesList),
            });
            throw new TRPCError({
              code: 'BAD_REQUEST',
              message: `Merge conflict in ${conflictFilesList.length} file(s)`,
              cause: { conflictFiles: conflictFilesList },
            });
          }
        }),

      // -----------------------------------------------------------------------
      // errand.delete
      // -----------------------------------------------------------------------
      delete: publicProcedure
        .input(z.object({ id: z.string().min(1) }))
        .mutation(async ({ ctx, input }) => {
          const repo = requireErrandRepository(ctx);
          const errand = await repo.findById(input.id);
          if (!errand) {
            throw new TRPCError({ code: 'NOT_FOUND', message: 'Errand not found' });
          }

          const agentManager = requireAgentManager(ctx);

          // Stop agent if active
          if (errand.status === 'active' && errand.agentId) {
            try { await agentManager.stop(errand.agentId); } catch { /* no-op */ }
          }

          // Remove worktree and branch (best-effort)
          const project = await requireProjectRepository(ctx).findById(errand.projectId);
          if (project) {
            const clonePath = await resolveClonePath(project, ctx);
            const worktreeManager = new SimpleGitWorktreeManager(clonePath);
            try { await worktreeManager.remove(errand.id); } catch { /* no-op if already gone */ }
            try { await requireBranchManager(ctx).deleteBranch(clonePath, errand.branch); } catch { /* no-op */ }
          }

          await repo.delete(errand.id);
          return { success: true };
        }),

      // -----------------------------------------------------------------------
      // errand.sendMessage
      // -----------------------------------------------------------------------
      sendMessage: publicProcedure
        .input(z.object({
          id: z.string().min(1),
          message: z.string().min(1),
        }))
        .mutation(async ({ ctx, input }) => {
          const errand = await requireErrandRepository(ctx).findById(input.id);
          if (!errand) {
            throw new TRPCError({ code: 'NOT_FOUND', message: 'Errand not found' });
          }

          if (errand.status !== 'active') {
            throw new TRPCError({ code: 'BAD_REQUEST', message: 'Errand is not active' });
          }

          if (!errand.agentId) {
            throw new TRPCError({ code: 'BAD_REQUEST', message: 'Errand has no associated agent' });
          }

          const agentManager = requireAgentManager(ctx);
          const agent = await agentManager.get(errand.agentId);
          if (!agent || agent.status === 'stopped' || agent.status === 'crashed') {
            const status = agent?.status ?? 'unknown';
            throw new TRPCError({
              code: 'BAD_REQUEST',
              message: `Agent is not running (status: ${status})`,
            });
          }

          await agentManager.sendUserMessage(errand.agentId, input.message);
          return { success: true };
        }),

      // -----------------------------------------------------------------------
      // errand.abandon
      // -----------------------------------------------------------------------
      abandon: publicProcedure
        .input(z.object({ id: z.string().min(1) }))
        .mutation(async ({ ctx, input }) => {
          const repo = requireErrandRepository(ctx);
          const errand = await repo.findById(input.id);
          if (!errand) {
            throw new TRPCError({ code: 'NOT_FOUND', message: 'Errand not found' });
          }

          if (errand.status === 'merged' || errand.status === 'abandoned') {
            throw new TRPCError({
              code: 'BAD_REQUEST',
              message: `Cannot abandon an errand with status '${errand.status}'`,
            });
          }

          const agentManager = requireAgentManager(ctx);
          const branchManager = requireBranchManager(ctx);

          // Stop agent if active
          if (errand.status === 'active' && errand.agentId) {
            try { await agentManager.stop(errand.agentId); } catch { /* no-op */ }
          }

          // Remove worktree and branch (best-effort)
          const project = await requireProjectRepository(ctx).findById(errand.projectId);
          if (project) {
            const clonePath = await resolveClonePath(project, ctx);
            const worktreeManager = new SimpleGitWorktreeManager(clonePath);
            try { await worktreeManager.remove(errand.id); } catch { /* no-op if already gone */ }
            try { await branchManager.deleteBranch(clonePath, errand.branch); } catch { /* no-op */ }
          }

          const updated = await repo.update(input.id, { status: 'abandoned' });
          return updated;
        }),
    }),
  };
}
