import { TRPCError } from "@trpc/server";
import { eq, and, desc, count } from "drizzle-orm";
import { db } from "~/server/db";
import {
  voiceEnrollments,
  voiceAnalyses,
  analysisJobs,
  analysisResults,
  subscriptions,
  normalizedAlerts,
} from "~/server/db/schema";
import { saveAudio, getAudioUrl, deleteFile } from "./voiceprint/storage";
import {
  preprocessAudio,
  detectSynthetic,
  matchVoice,
  generateEmbedding,
} from "./voiceprint/ml.engine";

type DetectionVerdict = "NATURAL" | "SYNTHETIC" | "UNCERTAIN";

interface AnalysisFilters {
  page?: number;
  limit?: number;
  verdict?: DetectionVerdict;
}

export async function getEnrollments(userId: string) {
  return db
    .select()
    .from(voiceEnrollments)
    .where(and(eq(voiceEnrollments.userId, userId), eq(voiceEnrollments.isActive, true)))
    .orderBy(desc(voiceEnrollments.createdAt));
}

export async function createEnrollment(userId: string, name: string, audioBase64: string) {
  const audioBuffer = Buffer.from(audioBase64, "base64");
  const { hash, filePath } = await saveAudio(userId, audioBuffer);

  const features = await preprocessAudio(audioBuffer);
  const embedding = await generateEmbedding(features);

  const [enrollment] = await db
    .insert(voiceEnrollments)
    .values({
      userId,
      name,
      voiceHash: embedding.hash,
      audioMetadata: { filePath, duration: features.duration, sampleRate: features.sampleRate },
    })
    .returning();

  return enrollment;
}

export async function deleteEnrollment(userId: string, enrollmentId: string) {
  const [enrollment] = await db
    .select()
    .from(voiceEnrollments)
    .where(and(eq(voiceEnrollments.id, enrollmentId), eq(voiceEnrollments.userId, userId)))
    .limit(1);

  if (!enrollment) {
    throw new TRPCError({ code: "NOT_FOUND", message: "Enrollment not found" });
  }

  const metadata = enrollment.audioMetadata as { filePath?: string } | null;
  if (metadata?.filePath) {
    await deleteFile(metadata.filePath);
  }

  const [deleted] = await db
    .update(voiceEnrollments)
    .set({ isActive: false, updatedAt: new Date() })
    .where(eq(voiceEnrollments.id, enrollmentId))
    .returning();

  return deleted;
}

function deriveVerdict(isSynthetic: boolean, confidence: number): DetectionVerdict {
  if (confidence >= 0.7) {
    return isSynthetic ? "SYNTHETIC" : "NATURAL";
  }
  return "UNCERTAIN";
}

async function createVoiceAlert(
  userId: string,
  analysisId: string,
  verdict: DetectionVerdict,
  confidence: number,
) {
  try {
    const [sub] = await db
      .select()
      .from(subscriptions)
      .where(eq(subscriptions.userId, userId))
      .limit(1);

    if (sub) {
      const category = verdict === "SYNTHETIC" ? "SYNTHETIC_VOICE" : "VOICE_MISMATCH";
      await db.insert(normalizedAlerts).values({
        source: "VOICEPRINT",
        category,
        severity: verdict === "SYNTHETIC" ? "HIGH" : "MEDIUM",
        userId,
        title: verdict === "SYNTHETIC" ? "Synthetic Voice Detected" : "Voice Mismatch Detected",
        description: `Analysis ${analysisId} returned verdict ${verdict} with ${(confidence * 100).toFixed(1)}% confidence`,
        entities: { analysisId, verdict, confidence },
        sourceAlertId: `voiceprint-${analysisId}`,
        createdAt: new Date(),
      });
    }
  } catch (err) {
    console.error("[voiceprint] Failed to create alert:", err);
  }
}

export async function analyzeAudio(
  userId: string,
  audioBase64: string,
  enrollmentId?: string,
) {
  const audioBuffer = Buffer.from(audioBase64, "base64");
  const { hash: audioHash, filePath } = await saveAudio(userId, audioBuffer);

  const features = await preprocessAudio(audioBuffer);
  const detection = await detectSynthetic(features);
  const audioUrl = getAudioUrl(userId, audioHash);

  let matchedEnrollmentId: string | null = null;
  let matchedSimilarity: number | null = null;

  if (enrollmentId) {
    const [enrollment] = await db
      .select()
      .from(voiceEnrollments)
      .where(
        and(
          eq(voiceEnrollments.id, enrollmentId),
          eq(voiceEnrollments.userId, userId),
          eq(voiceEnrollments.isActive, true),
        ),
      )
      .limit(1);

    if (enrollment) {
      const embedding = await generateEmbedding(features);
      const match = await matchVoice(embedding, enrollmentId);
      matchedEnrollmentId = enrollmentId;
      matchedSimilarity = match.similarity;
    }
  }

  const isSynthetic = detection.isSynthetic;
  const confidence = detection.confidence;
  const verdict = deriveVerdict(isSynthetic, confidence);

  const [analysis] = await db
    .insert(voiceAnalyses)
    .values({
      enrollmentId: matchedEnrollmentId ?? undefined,
      userId,
      audioHash,
      isSynthetic,
      confidence,
      analysisResult: { verdict, score: detection.score, matchedSimilarity },
      audioUrl,
    })
    .returning();

  if (verdict === "SYNTHETIC" || matchedSimilarity !== null) {
    await createVoiceAlert(userId, analysis.id, verdict, confidence);
  }

  return {
    id: analysis.id,
    verdict,
    confidence,
    isSynthetic,
    score: detection.score,
    matchedEnrollmentId,
    matchedSimilarity,
    audioUrl,
    createdAt: analysis.createdAt,
  };
}

export async function getAnalyses(
  userId: string,
  filters?: AnalysisFilters,
) {
  const page = filters?.page ?? 1;
  const limit = filters?.limit ?? 20;
  const offset = (page - 1) * limit;

  const conditions = [eq(voiceAnalyses.userId, userId)];
  if (filters?.verdict) {
    if (filters.verdict === "SYNTHETIC") {
      conditions.push(eq(voiceAnalyses.isSynthetic, true));
    } else if (filters.verdict === "NATURAL") {
      conditions.push(eq(voiceAnalyses.isSynthetic, false));
    }
  }

  const [totalResult] = await db
    .select({ count: count() })
    .from(voiceAnalyses)
    .where(and(...conditions));

  const items = await db
    .select()
    .from(voiceAnalyses)
    .where(and(...conditions))
    .orderBy(desc(voiceAnalyses.createdAt))
    .limit(limit)
    .offset(offset);

  return {
    items,
    total: totalResult.count,
    page,
    limit,
    totalPages: Math.ceil(totalResult.count / limit),
  };
}

export async function getAnalysisResult(userId: string, analysisId: string) {
  const [analysis] = await db
    .select()
    .from(voiceAnalyses)
    .where(and(eq(voiceAnalyses.id, analysisId), eq(voiceAnalyses.userId, userId)))
    .limit(1);

  if (!analysis) {
    throw new TRPCError({ code: "NOT_FOUND", message: "Analysis not found" });
  }

  return analysis;
}

export async function getJobStatus(userId: string, jobId: string) {
  const [job] = await db
    .select()
    .from(analysisJobs)
    .where(and(eq(analysisJobs.id, jobId), eq(analysisJobs.userId, userId)))
    .limit(1);

  if (!job) {
    throw new TRPCError({ code: "NOT_FOUND", message: "Job not found" });
  }

  let result = null;
  if (job.status === "COMPLETED") {
    const [r] = await db
      .select()
      .from(analysisResults)
      .where(eq(analysisResults.analysisJobId, jobId))
      .limit(1);
    result = r ?? null;
  }

  return {
    ...job,
    result,
  };
}

export async function createBatchJob(userId: string, audioFilePath: string) {
  const [job] = await db
    .insert(analysisJobs)
    .values({
      userId,
      analysisType: "BATCH",
      audioFilePath,
      status: "PENDING",
    })
    .returning();

  return job;
}
