import { headers } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
import { verifySigningToken, createSignerDownloadToken } from '@/lib/signing/token';
import { logAuditEvent } from '@/lib/signing/audit';
import { db } from '@/lib/db';
import { signingTokens, documents, clients, isClientVisibleField, getFieldType, DocumentSigner } from '@/lib/db/schema';
import { eq, isNull, and, sql } from 'drizzle-orm';
import path from 'node:path';
import { readFile, writeFile, unlink } from 'node:fs/promises';
import { PDFDocument, StandardFonts, rgb } from '@cantoo/pdf-lib';
import { embedSignatureInPdf } from '@/lib/signing/embed-signature';
import { sendAgentNotificationEmail, sendSignerCompletionEmail } from '@/lib/signing/signing-mailer';

const UPLOADS_DIR = path.join(process.cwd(), 'uploads');

// Public route — no auth session required
export async function GET(
  _req: NextRequest,
  { params }: { params: Promise<{ token: string }> }
) {
  const { token } = await params;

  // Extract IP and user-agent for audit logging
  const hdrs = await headers();
  const ip =
    hdrs.get('x-forwarded-for')?.split(',')[0]?.trim() ??
    hdrs.get('x-real-ip') ??
    'unknown';
  const ua = hdrs.get('user-agent') ?? 'unknown';

  // 1. Verify JWT
  let payload: { documentId: string; jti: string; exp: number };
  try {
    payload = await verifySigningToken(token);
  } catch (err: unknown) {
    // Check if it's a JWT expiry error
    const errName = err instanceof Error ? err.constructor.name : '';
    const errMessage = err instanceof Error ? err.message : '';
    if (errName === 'JWTExpired' || errMessage.includes('expired')) {
      return NextResponse.json({ status: 'expired' }, { status: 200 });
    }
    return NextResponse.json({ status: 'invalid' }, { status: 200 });
  }

  // 2. Look up jti in signingTokens table
  const tokenRow = await db.query.signingTokens.findFirst({
    where: eq(signingTokens.jti, payload.jti),
  });

  if (!tokenRow) {
    return NextResponse.json({ status: 'invalid' }, { status: 200 });
  }

  // 3. Check one-time use
  if (tokenRow.usedAt !== null) {
    return NextResponse.json(
      { status: 'used', signedAt: tokenRow.usedAt.toISOString() },
      { status: 200 }
    );
  }

  // 4. Fetch document
  const doc = await db.query.documents.findFirst({
    where: eq(documents.id, payload.documentId),
    columns: {
      id: true,
      name: true,
      signatureFields: true,
      preparedFilePath: true,
    },
  });

  if (!doc || !doc.preparedFilePath) {
    return NextResponse.json({ status: 'invalid' }, { status: 200 });
  }

  // 5 & 6. Log audit events + update status to Viewed
  await Promise.all([
    logAuditEvent({ documentId: payload.documentId, eventType: 'link_opened', ipAddress: ip, userAgent: ua }),
    logAuditEvent({ documentId: payload.documentId, eventType: 'document_viewed', ipAddress: ip, userAgent: ua }),
    db.update(documents).set({ status: 'Viewed' }).where(eq(documents.id, payload.documentId)),
  ]);

  // 7. Return pending state with document data
  return NextResponse.json({
    status: 'pending',
    document: {
      id: doc.id,
      name: doc.name,
      signatureFields: (doc.signatureFields ?? []).filter((field) => {
        if (!isClientVisibleField(field)) return false;
        // D-04: If token has signerEmail, only return this signer's fields
        if (tokenRow.signerEmail !== null) {
          return field.signerEmail === tokenRow.signerEmail;
        }
        // D-05: Legacy null-signer token — return all client-visible fields
        return true;
      }),
      preparedFilePath: doc.preparedFilePath,
    },
    expiresAt: new Date(payload.exp * 1000).toISOString(),
  });
}

// POST /api/sign/[token] — atomic signing submission
// Body: { signatures: Array<{ fieldId: string; dataURL: string }> }
// NEVER reads x/y/width/height from client — always uses server-stored signatureFields
export async function POST(
  req: NextRequest,
  { params }: { params: Promise<{ token: string }> }
) {
  const { token } = await params;

  // 1. Parse body
  const { signatures } = (await req.json()) as {
    signatures: Array<{ fieldId: string; dataURL: string }>;
  };

  // 2. Verify JWT
  let payload: { documentId: string; jti: string; exp: number };
  try {
    payload = await verifySigningToken(token);
  } catch {
    return NextResponse.json({ error: 'invalid-token' }, { status: 401 });
  }

  // Extract IP and user-agent for audit logging
  const hdrs = await headers();
  const ip = hdrs.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown';
  const ua = hdrs.get('user-agent') ?? 'unknown';

  // 3. ATOMIC ONE-TIME ENFORCEMENT
  // UPDATE signing_tokens SET used_at = NOW() WHERE jti = ? AND used_at IS NULL RETURNING jti
  // If 0 rows returned, token was already used — return 409, do NOT proceed to embed PDF
  const claimed = await db
    .update(signingTokens)
    .set({ usedAt: new Date() })
    .where(and(eq(signingTokens.jti, payload.jti), isNull(signingTokens.usedAt)))
    .returning({ jti: signingTokens.jti });

  if (claimed.length === 0) {
    return NextResponse.json({ error: 'already-signed' }, { status: 409 });
  }

  // 3.5. Fetch tokenRow to get signerEmail (already claimed above — safe to read)
  const tokenRow = await db.query.signingTokens.findFirst({
    where: eq(signingTokens.jti, payload.jti),
  });
  const signerEmail = tokenRow?.signerEmail ?? null;

  // 4. Log signature_submitted audit event (after atomic claim, before PDF work)
  await logAuditEvent({
    documentId: payload.documentId,
    eventType: 'signature_submitted',
    ipAddress: ip,
    userAgent: ua,
    ...(signerEmail ? { metadata: { signerEmail } } : {}),
  });

  // 5. Fetch document with signatureFields and preparedFilePath
  const doc = await db.query.documents.findFirst({
    where: eq(documents.id, payload.documentId),
    columns: {
      id: true,
      name: true,
      signatureFields: true,
      preparedFilePath: true,
      clientId: true,
    },
  });

  // 6. Guard: preparedFilePath must be set (should never happen after atomics, but defensive)
  if (!doc || !doc.preparedFilePath) {
    return NextResponse.json({ error: 'document-not-found' }, { status: 422 });
  }

  // 7. Build paths — accumulate pattern (D-10)
  // Re-fetch to get latest signedFilePath (another signer may have already updated it)
  const freshDoc = await db.query.documents.findFirst({
    where: eq(documents.id, payload.documentId),
    columns: { signedFilePath: true, preparedFilePath: true },
  });
  const workingRelPath = freshDoc?.signedFilePath ?? freshDoc?.preparedFilePath ?? doc.preparedFilePath!;
  const workingAbsPath = path.join(UPLOADS_DIR, workingRelPath);

  // Per-signer partial output path (unique by JTI — no collisions between concurrent signers)
  const partialRelPath = doc.preparedFilePath!.replace(/_prepared\.pdf$/, `_partial_${payload.jti}.pdf`);
  const partialAbsPath = path.join(UPLOADS_DIR, partialRelPath);

  // Path traversal guard (both paths)
  if (!workingAbsPath.startsWith(UPLOADS_DIR) || !partialAbsPath.startsWith(UPLOADS_DIR)) {
    return NextResponse.json({ error: 'forbidden' }, { status: 403 });
  }

  // Capture signing timestamp here — reused for date stamping (8a) and DB update (11)
  const now = new Date();

  // 8a. Stamp date text at each 'date' field — scoped to this signer's fields (D-09)
  const dateFields = (doc.signatureFields ?? []).filter((f) => {
    if (getFieldType(f) !== 'date') return false;
    if (!isClientVisibleField(f)) return false;
    if (signerEmail !== null) return f.signerEmail === signerEmail;
    return true; // legacy: stamp all date fields
  });

  // Only load and modify PDF if there are date fields to stamp
  let dateStampedPath = workingAbsPath;
  if (dateFields.length > 0) {
    const pdfBytes = await readFile(workingAbsPath);
    const pdfDoc = await PDFDocument.load(pdfBytes);
    const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica);
    const pages = pdfDoc.getPages();
    const signingDateStr = now.toLocaleDateString('en-US', {
      month: '2-digit', day: '2-digit', year: 'numeric',
    });
    for (const field of dateFields) {
      const page = pages[field.page - 1];
      if (!page) continue;
      // Draw signing date directly — no background rectangle so underlying PDF content shows through
      page.drawText(signingDateStr, {
        x: field.x + 4,
        y: field.y + field.height / 2 - 4,  // vertically center
        size: 10, font: helvetica, color: rgb(0.05, 0.05, 0.55),
      });
    }
    const stampedBytes = await pdfDoc.save();
    // JTI-keyed temp name prevents collision between concurrent signers
    dateStampedPath = `${workingAbsPath}.datestamped_${payload.jti}.tmp`;
    await writeFile(dateStampedPath, stampedBytes);
  }

  // 8b. Build signaturesWithCoords — scoped to this signer's signable fields (Pitfall 4)
  // text/checkbox/date are embedded at prepare time; the client was never shown these as interactive fields
  // CRITICAL: x/y/width/height come ONLY from the DB — never from the request body
  const signableFields = (doc.signatureFields ?? []).filter((f) => {
    const t = getFieldType(f);
    if (t !== 'client-signature' && t !== 'initials') return false;
    if (signerEmail !== null) return f.signerEmail === signerEmail;
    return isClientVisibleField(f); // legacy
  });

  const signaturesWithCoords = signableFields.map((field) => {
    const clientSig = signatures.find((s) => s.fieldId === field.id);
    if (!clientSig) {
      throw new Error(`Missing signature for field ${field.id}`);
    }
    return {
      fieldId: field.id,
      dataURL: clientSig.dataURL,
      x: field.x,
      y: field.y,
      width: field.width,
      height: field.height,
      page: field.page,
    };
  });

  // 9. Embed signatures into PDF — input: date-stamped working PDF, output: this signer's partial
  let pdfHash: string;
  try {
    pdfHash = await embedSignatureInPdf(dateStampedPath, partialAbsPath, signaturesWithCoords);
  } catch (err) {
    console.error('[sign/POST] embedSignatureInPdf failed:', err);
    return NextResponse.json({ error: 'pdf-embed-failed' }, { status: 500 });
  }

  // 9.5. Clean up temporary date-stamped file if it was created
  if (dateStampedPath !== workingAbsPath) {
    unlink(dateStampedPath).catch(() => {});
  }

  // 10. Log pdf_hash_computed audit event
  await logAuditEvent({
    documentId: payload.documentId,
    eventType: 'pdf_hash_computed',
    ipAddress: ip,
    userAgent: ua,
    metadata: { hash: pdfHash, signedFilePath: partialRelPath, ...(signerEmail ? { signerEmail } : {}) },
  });

  // 10.5. Update signedFilePath to this signer's partial (becomes working PDF for next signer)
  await db.update(documents)
    .set({ signedFilePath: partialRelPath })
    .where(eq(documents.id, payload.documentId));

  // 11. Completion detection (D-07, D-08)
  // Count remaining unclaimed tokens for this document
  const remaining = await db
    .select({ cnt: sql<number>`cast(count(*) as int)` })
    .from(signingTokens)
    .where(and(
      eq(signingTokens.documentId, payload.documentId),
      isNull(signingTokens.usedAt)
    ));
  const allDone = remaining[0].cnt === 0;

  if (!allDone) {
    // Not all signers done — return success for this signer without completing document
    return NextResponse.json({ ok: true });
  }

  // All tokens claimed — race for completionTriggeredAt (atomic guard)
  const won = await db
    .update(documents)
    .set({ completionTriggeredAt: now })
    .where(and(
      eq(documents.id, payload.documentId),
      isNull(documents.completionTriggeredAt)
    ))
    .returning({ id: documents.id });

  if (won.length === 0) {
    // Another handler won the race — still success for this signer
    return NextResponse.json({ ok: true });
  }

  // 12. Winner: finalize document — set status Signed, signedAt, final pdfHash
  await db
    .update(documents)
    .set({
      status: 'Signed',
      signedAt: now,
      signedFilePath: partialRelPath, // already set in 10.5, confirm final value
      pdfHash,
    })
    .where(eq(documents.id, payload.documentId));

  // 13. Fire-and-forget: agent notification + signer completion emails (completion winner only)
  const baseUrl = process.env.APP_BASE_URL ?? 'http://localhost:3000';

  // Agent notification email
  db.query.documents
    .findFirst({
      where: eq(documents.id, payload.documentId),
      with: { client: { columns: { name: true } } },
      columns: { name: true, signers: true },
    })
    .then(async (notifDoc) => {
      try {
        const signers = (notifDoc?.signers as DocumentSigner[] | null) ?? [];
        const clientName = signers.length > 0
          ? `${signers.length} signers`
          : ((notifDoc as { client?: { name: string } } & typeof notifDoc)?.client?.name ?? 'Client');
        await sendAgentNotificationEmail({
          clientName,
          documentName: notifDoc?.name ?? doc.name,
          signedAt: now,
        });
      } catch (emailErr) {
        console.error('[sign/POST] agent notification email failed (non-fatal):', emailErr);
      }
    })
    .catch((err) => {
      console.error('[sign/POST] agent notification fetch failed (non-fatal):', err);
    });

  // Signer completion emails — send to all signers with download link (MSIGN-11)
  db.query.documents
    .findFirst({
      where: eq(documents.id, payload.documentId),
      columns: { name: true, signers: true },
    })
    .then(async (completionDoc) => {
      try {
        const signers = (completionDoc?.signers as DocumentSigner[] | null) ?? [];
        if (signers.length === 0) return; // legacy single-signer — no signer completion emails

        const downloadToken = await createSignerDownloadToken(payload.documentId);
        const downloadUrl = `${baseUrl}/api/sign/download/${downloadToken}`;

        await Promise.all(
          signers.map((signer) =>
            sendSignerCompletionEmail({
              to: signer.email,
              documentName: completionDoc?.name ?? doc.name,
              downloadUrl,
            }).catch((err) => {
              console.error(`[sign/POST] signer completion email to ${signer.email} failed (non-fatal):`, err);
            })
          )
        );
      } catch (err) {
        console.error('[sign/POST] signer completion emails failed (non-fatal):', err);
      }
    })
    .catch((err) => {
      console.error('[sign/POST] signer completion fetch failed (non-fatal):', err);
    });

  // 14. Return 200 — client redirects to /sign/[token]/confirmed
  return NextResponse.json({ ok: true });
}
