// Copyright 2026 CRMy Contributors
// SPDX-License-Identifier: Apache-2.0

import { z } from 'zod';
import {
  assignmentCreate, assignmentGet, assignmentSearch,
  assignmentUpdate, assignmentAccept, assignmentComplete, assignmentDecline,
  assignmentStart, assignmentBlock, assignmentCancel,
} from '@crmy/shared';
import type { DbPool } from '../../db/pool.js';
import type { ActionContext, ActionContextProposedAction, ActorContext, SubjectType, UUID } from '@crmy/shared';
import * as assignmentRepo from '../../db/repos/assignments.js';
import * as governorLimits from '../../db/repos/governor-limits.js';
import { emitEvent } from '../../events/emitter.js';
import { notFound, validationError } from '@crmy/shared';
import { validateAssignmentAction } from '../../services/state-machine.js';
import { runIdempotent } from '../../db/repos/idempotency.js';
import { mutationReceipt } from '../mutation-receipt.js';
import { indexDocument } from '../../search/SearchIndexerService.js';
import type { ToolDef } from '../server.js';
import {
  assertActivityAccess,
  assertAssignmentAccess,
  assertSubjectAccess,
  filterVisibleAssignments,
  isGlobalActor,
} from '../../services/access-control.js';
import { getActionContext, verifiedActionContextMetadataForReceipt } from '../../services/action-context.js';

function runAssignmentOperation<T>(
  db: DbPool,
  actor: ActorContext,
  operation: string,
  input: { idempotency_key?: string },
  fn: () => Promise<T>,
): Promise<T> {
  return runIdempotent(db, {
    tenantId: actor.tenant_id,
    actorId: actor.actor_id,
    operation,
    key: input.idempotency_key,
    request: input,
  }, fn);
}

const assignmentSubjectTables = {
  contact: { table: 'contacts', label: 'contact' },
  account: { table: 'accounts', label: 'account' },
  opportunity: { table: 'opportunities', label: 'opportunity' },
  use_case: { table: 'use_cases', label: 'use case' },
} as const;

async function assertAssignmentSubjectExists(
  db: DbPool,
  tenantId: string,
  subjectType: keyof typeof assignmentSubjectTables,
  subjectId: string,
) {
  const config = assignmentSubjectTables[subjectType];
  const result = await db.query(
    `SELECT 1 FROM ${config.table} WHERE tenant_id = $1 AND id = $2 LIMIT 1`,
    [tenantId, subjectId],
  );
  if (result.rowCount === 0) {
    throw validationError(`Selected ${config.label} does not exist`, [{ field: 'subject_id', message: `Choose an existing ${config.label}` }]);
  }
}

async function assertActorExists(db: DbPool, tenantId: string, actorId: string) {
  const result = await db.query('SELECT 1 FROM actors WHERE tenant_id = $1 AND id = $2 LIMIT 1', [tenantId, actorId]);
  if (result.rowCount === 0) {
    throw validationError('Selected assignee does not exist', [{ field: 'assigned_to', message: 'Choose an existing actor' }]);
  }
}

async function validateAssignmentSubject(
  db: DbPool,
  actor: ActorContext,
  subjectType?: keyof typeof assignmentSubjectTables | null,
  subjectId?: string | null,
) {
  if (!subjectType && !subjectId) return;
  if (!subjectType || !subjectId) {
    throw validationError('Choose both a linked record type and a record, or leave the link empty', [
      { field: 'subject_id', message: 'Choose a record for the selected type' },
    ]);
  }
  await assertAssignmentSubjectExists(db, actor.tenant_id, subjectType, subjectId);
  await assertSubjectAccess(db, actor, subjectType, subjectId);
}

function queueAssignmentIndex(db: DbPool, assignment: Record<string, unknown>) {
  indexDocument(db, 'assignment', assignment).catch(() => {});
}

function sanitizeAssignmentMetadata(metadata: unknown): Record<string, unknown> {
  const record = metadata && typeof metadata === 'object' && !Array.isArray(metadata)
    ? { ...(metadata as Record<string, unknown>) }
    : {};
  delete record.action_context;
  return record;
}

function summarizeAssignmentActionContext(actionContext: ActionContext) {
  return {
    subject_type: actionContext.subject_type,
    subject_id: actionContext.subject_id,
    operating_mode: actionContext.operating_mode,
    readiness_status: actionContext.readiness.status,
    risk_level: actionContext.readiness.risk_level,
    review_required: actionContext.readiness.review_required,
    guidance_summary: actionContext.guidance.summary,
    warning_reasons: actionContext.guidance.warning_reasons,
    review_reasons: actionContext.guidance.review_reasons,
    proof: actionContext.proof,
  };
}

async function getAssignmentActionContext(
  db: DbPool,
  actor: ActorContext,
  input: { subject_type?: SubjectType | null; subject_id?: UUID | string | null; assignment_type?: string; title?: string; priority?: string },
): Promise<ActionContext | null> {
  if (!input.subject_type || !input.subject_id) return null;
  const proposedAction: ActionContextProposedAction = {
    action_type: 'assignment_create',
    object_type: input.subject_type,
    payload: {
      assignment_type: input.assignment_type,
      title: input.title,
      priority: input.priority,
    },
  };
  return getActionContext(db, actor, {
    subject_type: input.subject_type,
    subject_id: input.subject_id as UUID,
    context_radius: input.subject_type === 'account' ? 'account_wide' : 'adjacent',
    token_budget: 1800,
    proposed_action: proposedAction,
  });
}

export function assignmentTools(db: DbPool): ToolDef[] {
  return [
    {
      name: 'assignment_create',
      tier: 'core',
      description: 'Create an assignment to hand off work from one actor to another — this is the agent-to-human (or agent-to-agent) coordination primitive. The context field (plain text) is the handoff brief: write it as if explaining to a colleague what they need to know — what you tried, what you learned, what the assignee needs to do, and what to avoid. Set priority to "urgent" sparingly as it surfaces at the top of the human queue. The human sees assignments in the web UI Assignments view and via "crmy assignments list --mine". The assigner is automatically set to the current actor.',
      inputSchema: assignmentCreate,
      handler: async (input: z.infer<typeof assignmentCreate>, actor: ActorContext) => {
        return runAssignmentOperation(db, actor, 'assignment_create', input, async () => {
        // Enforce governor limit on active assignments
        const activeCount = await governorLimits.countActiveAssignments(db, actor.tenant_id);
        await governorLimits.enforceLimit(db, actor.tenant_id, 'assignments_active', activeCount);

        await assertActorExists(db, actor.tenant_id, input.assigned_to);
        await validateAssignmentSubject(db, actor, input.subject_type, input.subject_id);
        const actionContext = await getAssignmentActionContext(db, actor, input);
        const actionContextMetadata = actionContext ? summarizeAssignmentActionContext(actionContext) : undefined;

        const assignment = await assignmentRepo.createAssignment(db, actor.tenant_id, {
          ...input,
          assigned_by: actor.actor_id,
          metadata: {
            ...sanitizeAssignmentMetadata(input.metadata),
            ...(actionContextMetadata ? { action_context: actionContextMetadata } : {}),
          },
        });
        queueAssignmentIndex(db, assignment as unknown as Record<string, unknown>);
        const event_id = await emitEvent(db, {
          tenantId: actor.tenant_id,
          eventType: 'assignment.created',
          actorId: actor.actor_id,
          actorType: actor.actor_type,
          objectType: 'assignment',
          objectId: assignment.id,
          afterData: assignment,
          metadata: actionContextMetadata ? { action_context: actionContextMetadata } : undefined,
        });
        return {
          assignment,
          event_id,
          mutation: mutationReceipt(actor, {
            objectType: 'assignment',
            objectId: assignment.id,
            eventId: event_id,
          }),
        };
        });
      },
    },
    {
      name: 'assignment_get',
      tier: 'core',
      description: 'Retrieve a single assignment by UUID, including its full context brief, status, priority, and linked subject. Use this to read the handoff details before starting work on an assignment.',
      inputSchema: assignmentGet,
      handler: async (input: z.infer<typeof assignmentGet>, actor: ActorContext) => {
        const assignment = await assignmentRepo.getAssignment(db, actor.tenant_id, input.id);
        if (!assignment) throw notFound('Assignment', input.id);
        await assertAssignmentAccess(db, actor, assignment);
        return { assignment };
      },
    },
    {
      name: 'assignment_list',
      tier: 'core',
      description: 'List assignments with flexible filters. Use assigned_to to see your own queue, assigned_by to see what you have delegated, status to filter by lifecycle state (pending, accepted, in_progress, blocked, completed, declined, cancelled), and priority to focus on urgent items. Returns paginated results sorted by priority and creation time.',
      inputSchema: assignmentSearch,
      handler: async (input: z.infer<typeof assignmentSearch>, actor: ActorContext) => {
        const result = await assignmentRepo.searchAssignments(db, actor.tenant_id, {
          ...input,
          limit: Math.min((input.limit ?? 20) * 3, 100),
        });
        const visible = await filterVisibleAssignments(db, actor, result.data, input.limit ?? 20);
        return { assignments: visible, next_cursor: result.next_cursor, total: isGlobalActor(actor) ? result.total : visible.length };
      },
    },
    {
      name: 'assignment_update',
      tier: 'extended',
      description: 'Update an assignment by passing its id and a patch object with fields to change. Use this to modify the context brief, adjust priority, update the due date, or change the assignee. For status transitions, prefer the dedicated accept/start/complete/block/decline/cancel tools instead.',
      inputSchema: assignmentUpdate,
      handler: async (input: z.infer<typeof assignmentUpdate>, actor: ActorContext) => {
        return runAssignmentOperation(db, actor, 'assignment_update', input, async () => {
        const before = await assignmentRepo.getAssignment(db, actor.tenant_id, input.id);
        if (!before) throw notFound('Assignment', input.id);
        await assertAssignmentAccess(db, actor, before);
        const submittedActionContext = input.action_context;

        if (input.patch.assigned_to) {
          await assertActorExists(db, actor.tenant_id, input.patch.assigned_to);
        }

        if ('subject_type' in input.patch || 'subject_id' in input.patch) {
          await validateAssignmentSubject(
            db,
            actor,
            input.patch.subject_type as keyof typeof assignmentSubjectTables | null | undefined,
            input.patch.subject_id as string | null | undefined,
          );
        }
        if (input.patch.metadata) {
          input.patch.metadata = sanitizeAssignmentMetadata(input.patch.metadata);
        }

        const assignment = await assignmentRepo.updateAssignment(db, actor.tenant_id, input.id, input.patch);
        if (!assignment) throw notFound('Assignment', input.id);
        queueAssignmentIndex(db, assignment as unknown as Record<string, unknown>);
        const actionContextMetadata = assignment.subject_type && assignment.subject_id
          ? await verifiedActionContextMetadataForReceipt(db, actor, assignment.subject_type, assignment.subject_id, submittedActionContext)
          : undefined;

        const event_id = await emitEvent(db, {
          tenantId: actor.tenant_id,
          eventType: 'assignment.updated',
          actorId: actor.actor_id,
          actorType: actor.actor_type,
          objectType: 'assignment',
          objectId: assignment.id,
          beforeData: before,
          afterData: assignment,
          metadata: actionContextMetadata ? { action_context: actionContextMetadata } : undefined,
        });
        return {
          assignment,
          event_id,
          mutation: mutationReceipt(actor, {
            objectType: 'assignment',
            objectId: assignment.id,
            eventId: event_id,
          }),
        };
        });
      },
    },
    {
      name: 'assignment_accept',
      tier: 'core',
      description: 'Accept a pending assignment, transitioning it from "pending" to "accepted" status. Call this when you are ready to take ownership of the work. The assignment must be in "pending" status.',
      inputSchema: assignmentAccept,
      handler: async (input: z.infer<typeof assignmentAccept>, actor: ActorContext) => {
        return runAssignmentOperation(db, actor, 'assignment_accept', input, async () => {
        const before = await assignmentRepo.getAssignment(db, actor.tenant_id, input.id);
        if (!before) throw notFound('Assignment', input.id);
        await assertAssignmentAccess(db, actor, before);

        const acceptBlocker = validateAssignmentAction('accept', before.status);
        if (acceptBlocker) throw validationError(acceptBlocker);

        const assignment = await assignmentRepo.acceptAssignment(db, actor.tenant_id, input.id);
        if (!assignment) throw notFound('Assignment', input.id);
        queueAssignmentIndex(db, assignment as unknown as Record<string, unknown>);

        const event_id = await emitEvent(db, {
          tenantId: actor.tenant_id,
          eventType: 'assignment.accepted',
          actorId: actor.actor_id,
          actorType: actor.actor_type,
          objectType: 'assignment',
          objectId: assignment.id,
          beforeData: { status: before.status },
          afterData: { status: 'accepted' },
        });
        return {
          assignment,
          event_id,
          mutation: mutationReceipt(actor, {
            objectType: 'assignment',
            objectId: assignment.id,
            eventId: event_id,
          }),
        };
        });
      },
    },
    {
      name: 'assignment_complete',
      tier: 'core',
      description: 'Mark an assignment as completed. Optionally pass completed_by_activity_id to link the activity that fulfilled the assignment — this creates a clear audit trail showing exactly what action completed the work.',
      inputSchema: assignmentComplete,
      handler: async (input: z.infer<typeof assignmentComplete>, actor: ActorContext) => {
        return runAssignmentOperation(db, actor, 'assignment_complete', input, async () => {
        const before = await assignmentRepo.getAssignment(db, actor.tenant_id, input.id);
        if (!before) throw notFound('Assignment', input.id);
        await assertAssignmentAccess(db, actor, before);
        if (input.completed_by_activity_id) {
          await assertActivityAccess(db, actor, input.completed_by_activity_id);
        }

        const completeBlocker = validateAssignmentAction('complete', before.status);
        if (completeBlocker) throw validationError(completeBlocker);

        const assignment = await assignmentRepo.completeAssignment(
          db, actor.tenant_id, input.id, input.completed_by_activity_id,
        );
        if (!assignment) throw notFound('Assignment', input.id);
        queueAssignmentIndex(db, assignment as unknown as Record<string, unknown>);

        const event_id = await emitEvent(db, {
          tenantId: actor.tenant_id,
          eventType: 'assignment.completed',
          actorId: actor.actor_id,
          actorType: actor.actor_type,
          objectType: 'assignment',
          objectId: assignment.id,
          beforeData: { status: before.status },
          afterData: { status: 'completed' },
        });
        return {
          assignment,
          event_id,
          mutation: mutationReceipt(actor, {
            objectType: 'assignment',
            objectId: assignment.id,
            eventId: event_id,
          }),
        };
        });
      },
    },
    {
      name: 'assignment_decline',
      tier: 'core',
      description: 'Decline a pending assignment you cannot or should not handle. Optionally provide a reason so the assigner understands why and can reassign. The reason is stored in the assignment metadata for audit.',
      inputSchema: assignmentDecline,
      handler: async (input: z.infer<typeof assignmentDecline>, actor: ActorContext) => {
        return runAssignmentOperation(db, actor, 'assignment_decline', input, async () => {
        const before = await assignmentRepo.getAssignment(db, actor.tenant_id, input.id);
        if (!before) throw notFound('Assignment', input.id);
        await assertAssignmentAccess(db, actor, before);

        const declineBlocker = validateAssignmentAction('decline', before.status);
        if (declineBlocker) throw validationError(declineBlocker);

        const assignment = await assignmentRepo.declineAssignment(db, actor.tenant_id, input.id);
        if (!assignment) throw notFound('Assignment', input.id);

        // If a reason was given, store it in metadata
        let indexedAssignment = assignment;
        if (input.reason) {
          indexedAssignment = await assignmentRepo.updateAssignment(db, actor.tenant_id, input.id, {
            metadata: { ...((assignment.metadata as Record<string, unknown>) ?? {}), decline_reason: input.reason },
          }) ?? assignment;
        }
        queueAssignmentIndex(db, indexedAssignment as unknown as Record<string, unknown>);

        const event_id = await emitEvent(db, {
          tenantId: actor.tenant_id,
          eventType: 'assignment.declined',
          actorId: actor.actor_id,
          actorType: actor.actor_type,
          objectType: 'assignment',
          objectId: assignment.id,
          beforeData: { status: before.status },
          afterData: { status: 'declined' },
          metadata: input.reason ? { reason: input.reason } : undefined,
        });
        return {
          assignment,
          event_id,
          mutation: mutationReceipt(actor, {
            objectType: 'assignment',
            objectId: assignment.id,
            eventId: event_id,
          }),
        };
        });
      },
    },
    {
      name: 'assignment_start',
      tier: 'core',
      description: 'Begin working on an accepted assignment, transitioning it from "accepted" to "in_progress" status. Call this when you actively start the work so the assigner can see progress.',
      inputSchema: assignmentStart,
      handler: async (input: z.infer<typeof assignmentStart>, actor: ActorContext) => {
        return runAssignmentOperation(db, actor, 'assignment_start', input, async () => {
        const before = await assignmentRepo.getAssignment(db, actor.tenant_id, input.id);
        if (!before) throw notFound('Assignment', input.id);
        await assertAssignmentAccess(db, actor, before);

        const startBlocker = validateAssignmentAction('start', before.status);
        if (startBlocker) throw validationError(startBlocker);

        const assignment = await assignmentRepo.startAssignment(db, actor.tenant_id, input.id);
        if (!assignment) throw notFound('Assignment', input.id);
        queueAssignmentIndex(db, assignment as unknown as Record<string, unknown>);

        const event_id = await emitEvent(db, {
          tenantId: actor.tenant_id,
          eventType: 'assignment.started',
          actorId: actor.actor_id,
          actorType: actor.actor_type,
          objectType: 'assignment',
          objectId: assignment.id,
          beforeData: { status: before.status },
          afterData: { status: 'in_progress' },
        });
        return {
          assignment,
          event_id,
          mutation: mutationReceipt(actor, {
            objectType: 'assignment',
            objectId: assignment.id,
            eventId: event_id,
          }),
        };
        });
      },
    },
    {
      name: 'assignment_block',
      tier: 'admin',
      description: 'Mark an in-progress assignment as blocked when you cannot continue without external input or resolution. Provide a reason describing what is blocking progress so the assigner or team can help unblock.',
      inputSchema: assignmentBlock,
      handler: async (input: z.infer<typeof assignmentBlock>, actor: ActorContext) => {
        return runAssignmentOperation(db, actor, 'assignment_block', input, async () => {
        const before = await assignmentRepo.getAssignment(db, actor.tenant_id, input.id);
        if (!before) throw notFound('Assignment', input.id);
        await assertAssignmentAccess(db, actor, before);

        const blockBlocker = validateAssignmentAction('block', before.status);
        if (blockBlocker) throw validationError(blockBlocker);

        const assignment = await assignmentRepo.blockAssignment(db, actor.tenant_id, input.id);
        if (!assignment) throw notFound('Assignment', input.id);

        let indexedAssignment = assignment;
        if (input.reason) {
          indexedAssignment = await assignmentRepo.updateAssignment(db, actor.tenant_id, input.id, {
            metadata: { ...((assignment.metadata as Record<string, unknown>) ?? {}), block_reason: input.reason },
          }) ?? assignment;
        }
        queueAssignmentIndex(db, indexedAssignment as unknown as Record<string, unknown>);

        const event_id = await emitEvent(db, {
          tenantId: actor.tenant_id,
          eventType: 'assignment.blocked',
          actorId: actor.actor_id,
          actorType: actor.actor_type,
          objectType: 'assignment',
          objectId: assignment.id,
          beforeData: { status: before.status },
          afterData: { status: 'blocked' },
          metadata: input.reason ? { reason: input.reason } : undefined,
        });
        return {
          assignment,
          event_id,
          mutation: mutationReceipt(actor, {
            objectType: 'assignment',
            objectId: assignment.id,
            eventId: event_id,
          }),
        };
        });
      },
    },
    {
      name: 'assignment_cancel',
      tier: 'admin',
      description: 'Cancel an assignment that is no longer needed. Works from any non-terminal state (pending, accepted, in_progress, blocked). Optionally provide a reason explaining why the work is no longer required.',
      inputSchema: assignmentCancel,
      handler: async (input: z.infer<typeof assignmentCancel>, actor: ActorContext) => {
        return runAssignmentOperation(db, actor, 'assignment_cancel', input, async () => {
        const before = await assignmentRepo.getAssignment(db, actor.tenant_id, input.id);
        if (!before) throw notFound('Assignment', input.id);
        await assertAssignmentAccess(db, actor, before);

        const cancelBlocker = validateAssignmentAction('cancel', before.status);
        if (cancelBlocker) throw validationError(cancelBlocker);

        const assignment = await assignmentRepo.cancelAssignment(db, actor.tenant_id, input.id);
        if (!assignment) throw notFound('Assignment', input.id);

        let indexedAssignment = assignment;
        if (input.reason) {
          indexedAssignment = await assignmentRepo.updateAssignment(db, actor.tenant_id, input.id, {
            metadata: { ...((assignment.metadata as Record<string, unknown>) ?? {}), cancel_reason: input.reason },
          }) ?? assignment;
        }
        queueAssignmentIndex(db, indexedAssignment as unknown as Record<string, unknown>);

        const event_id = await emitEvent(db, {
          tenantId: actor.tenant_id,
          eventType: 'assignment.cancelled',
          actorId: actor.actor_id,
          actorType: actor.actor_type,
          objectType: 'assignment',
          objectId: assignment.id,
          beforeData: { status: before.status },
          afterData: { status: 'cancelled' },
          metadata: input.reason ? { reason: input.reason } : undefined,
        });
        return {
          assignment,
          event_id,
          mutation: mutationReceipt(actor, {
            objectType: 'assignment',
            objectId: assignment.id,
            eventId: event_id,
          }),
        };
        });
      },
    },
  ];
}
