import { createHash } from 'node:crypto';
import { bandForScore } from '@pig/core';
import type { AgentTaskKind } from '@pig/core';
import {
  accounts,
  agentActions,
  contacts,
  facts,
  type AgentTask,
  type Database,
} from '@pig/db';
import { eq, or } from 'drizzle-orm';
import { z } from 'zod';
import { defineTool, type AgentTool } from './provider';

export const PIG_TOOL_NAMES = ['pig_get_subject', 'pig_record_fact'] as const;

interface PigToolContext {
  task: AgentTask;
  agentRunId: string;
}

const noInput = z.object({}).strict();
export const recordFactInput = z
  .object({
    targetType: z.enum(['account', 'contact']),
    targetId: z.string().uuid(),
    field: z.string().trim().min(1).max(100),
    value: z.string().trim().min(1).max(8_000),
    score: z.number().min(0).max(1),
    sourceUrl: z.string().url(),
    evidenceExcerpt: z.string().trim().min(1).max(4_000),
    observedAt: z.string().datetime().optional(),
    method: z.enum(['inference', 'prime_api', 'document']).default('inference'),
  })
  .strict();

export function createPigTools(db: Database, context: PigToolContext): AgentTool[] {
  return [
    defineTool({
      name: 'pig_get_subject',
      description:
        'Read the PIG record and existing evidence for the subject of this queued task. ' +
        'This tool cannot read arbitrary records.',
      inputSchema: noInput,
      execute: async () => readSubject(db, context.task),
    }),
    defineTool({
      name: 'pig_record_fact',
      description:
        'Propose an evidence-bearing fact about this task subject. This never mutates the ' +
        'account or contact directly and requires both a source URL and evidence excerpt.',
      inputSchema: recordFactInput,
      execute: async (input) => {
        const target = factTarget(context.task);
        if (!target || input.targetType !== target.type || input.targetId !== target.id) {
          throw new Error('Facts may only target the account or contact named by this task.');
        }

        const band = bandForScore(input.score);
        const idempotencyKey = factIdempotencyKey(context.task.id, input);

        return db.transaction(async (tx) => {
          const [action] = await tx
            .insert(agentActions)
            .values({
              agentRunId: context.agentRunId,
              type: 'record_fact',
              targetType: input.targetType,
              targetId: input.targetId,
              summary: `${input.field}: ${input.value}`.slice(0, 500),
              idempotencyKey,
              metadata: { taskId: context.task.id, field: input.field },
            })
            // This is backed by agent_actions_idempotency_key; retries must not
            // turn one model claim into multiple review-queue entries.
            .onConflictDoNothing({ target: agentActions.idempotencyKey })
            .returning({ id: agentActions.id });

          if (!action) return { created: false, duplicate: true };

          const [fact] = await tx
            .insert(facts)
            .values({
              ...(input.targetType === 'account'
                ? { accountId: input.targetId }
                : { contactId: input.targetId }),
              field: input.field,
              value: input.value,
              score: input.score.toFixed(3),
              band,
              // Automatic application needs a field-aware service. Until that
              // exists, even high-confidence claims remain reviewable instead
              // of silently changing commercially important records.
              status: 'proposed',
              evidence: {
                excerpt: input.evidenceExcerpt,
                taskId: context.task.id,
                taskReason: context.task.reason,
              },
              sourceUrl: input.sourceUrl,
              method: input.method,
              agentRunId: context.agentRunId,
              ...(input.observedAt ? { observedAt: new Date(input.observedAt) } : {}),
            })
            .returning({ id: facts.id });

          await tx
            .update(agentActions)
            .set({
              status: 'completed',
              externalId: fact!.id,
              metadata: { taskId: context.task.id, field: input.field, factId: fact!.id },
            })
            .where(eq(agentActions.id, action.id));

          return { created: true, factId: fact!.id, band, status: 'proposed' as const };
        });
      },
    }),
  ];
}

async function readSubject(db: Database, task: AgentTask): Promise<Record<string, unknown>> {
  const target = factTarget(task);
  if (!target) return { task: { kind: task.kind, subject: task.subject, payload: task.payload } };

  if (target.type === 'account') {
    const [account] = await db.select().from(accounts).where(eq(accounts.id, target.id)).limit(1);
    if (!account) throw new Error('Task account no longer exists.');

    const relatedContacts = await db
      .select({
        id: contacts.id,
        fullName: contacts.fullName,
        title: contacts.title,
        affiliation: contacts.affiliation,
        confidence: contacts.confidence,
        sourceUrl: contacts.sourceUrl,
      })
      .from(contacts)
      .where(eq(contacts.accountId, target.id));
    const existingFacts = await db.select().from(facts).where(eq(facts.accountId, target.id));
    return { account, contacts: relatedContacts, facts: existingFacts, payload: task.payload };
  }

  const [contact] = await db.select().from(contacts).where(eq(contacts.id, target.id)).limit(1);
  if (!contact) throw new Error('Task contact no longer exists.');
  const [account] = contact.accountId
    ? await db.select().from(accounts).where(eq(accounts.id, contact.accountId)).limit(1)
    : [];
  const existingFacts = await db
    .select()
    .from(facts)
    .where(or(eq(facts.contactId, target.id), eq(facts.accountId, contact.accountId ?? target.id)));
  return { contact, account: account ?? null, facts: existingFacts, payload: task.payload };
}

function factTarget(task: AgentTask): { type: 'account' | 'contact'; id: string } | null {
  const accountKinds: AgentTaskKind[] = ['enrich_account', 'research_supplier'];
  if (accountKinds.includes(task.kind)) return { type: 'account', id: task.subject };
  if (task.kind === 'enrich_contact') return { type: 'contact', id: task.subject };
  return null;
}

function factIdempotencyKey(taskId: string, input: z.infer<typeof recordFactInput>): string {
  const digest = createHash('sha256')
    .update(
      JSON.stringify([
        input.targetType,
        input.targetId,
        input.field,
        input.value,
        input.sourceUrl,
        input.evidenceExcerpt,
      ]),
    )
    .digest('hex');
  return `piggy:fact:${taskId}:${digest}`;
}
