import { createHash } from "node:crypto";
import { TRPCError } from "@trpc/server";
import { eq, and, desc, count, gte, lte, inArray, sql } from "drizzle-orm";
import { db } from "~/server/db";
import { watchlistItems, exposures, subscriptions, securityReports } from "~/server/db/schema";
import { scanHIBP, scanSecurityTrails, scanCensys, scanShodan, scanForums } from "./darkwatch/scan.engine";
import { processExposure } from "./darkwatch/alert.pipeline";
import type { ScanResult } from "./darkwatch/scan.engine";

interface ScanState {
  status: "idle" | "running" | "completed" | "failed";
  startedAt: Date | null;
  completedAt: Date | null;
  totalSources: number;
  completedSources: number;
  error: string | null;
}

const scanStates = new Map<string, ScanState>();

function hashValue(value: string): string {
  return createHash("sha256").update(value.toLowerCase().trim()).digest("hex");
}

async function getSubscription(userId: string) {
  const [sub] = await db
    .select()
    .from(subscriptions)
    .where(and(eq(subscriptions.userId, userId), eq(subscriptions.status, "active")))
    .limit(1);
  if (!sub) {
    throw new TRPCError({ code: "NOT_FOUND", message: "No active subscription found" });
  }
  return sub;
}

export async function getWatchlistItems(userId: string) {
  const sub = await getSubscription(userId);
  const items = await db
    .select()
    .from(watchlistItems)
    .where(and(eq(watchlistItems.subscriptionId, sub.id), eq(watchlistItems.isActive, true)))
    .orderBy(desc(watchlistItems.createdAt));
  return items;
}

export async function addWatchlistItem(userId: string, type: string, value: string) {
  const sub = await getSubscription(userId);
  const hash = hashValue(value);

  const [existing] = await db
    .select()
    .from(watchlistItems)
    .where(
      and(
        eq(watchlistItems.subscriptionId, sub.id),
        eq(watchlistItems.type, type as "email" | "phoneNumber" | "ssn" | "address" | "domain"),
        eq(watchlistItems.hash, hash),
      ),
    )
    .limit(1);

  if (existing) {
    if (existing.isActive) {
      throw new TRPCError({ code: "CONFLICT", message: "Watchlist item already exists" });
    }
    const [reactivated] = await db
      .update(watchlistItems)
      .set({ isActive: true, updatedAt: new Date() })
      .where(eq(watchlistItems.id, existing.id))
      .returning();
    return reactivated;
  }

  const [inserted] = await db
    .insert(watchlistItems)
    .values({
      subscriptionId: sub.id,
      type: type as "email" | "phoneNumber" | "ssn" | "address" | "domain",
      value,
      hash,
    })
    .returning();
  return inserted;
}

export async function removeWatchlistItem(userId: string, itemId: string) {
  const sub = await getSubscription(userId);
  const [item] = await db
    .select()
    .from(watchlistItems)
    .where(and(eq(watchlistItems.id, itemId), eq(watchlistItems.subscriptionId, sub.id)))
    .limit(1);

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

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

export async function getExposures(
  userId: string,
  filters?: {
    severity?: string;
    source?: string;
    page?: number;
    limit?: number;
  },
) {
  const sub = await getSubscription(userId);
  const page = filters?.page ?? 1;
  const limit = filters?.limit ?? 20;
  const offset = (page - 1) * limit;

  const conditions = [eq(exposures.subscriptionId, sub.id)];
  if (filters?.severity) {
    conditions.push(eq(exposures.severity, filters.severity as "info" | "warning" | "critical"));
  }
  if (filters?.source) {
    conditions.push(eq(exposures.source, filters.source as "hibp" | "securityTrails" | "censys" | "darkWebForum" | "shodan" | "honeypot"));
  }

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

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

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

export async function getExposureDetails(userId: string, exposureId: string) {
  const sub = await getSubscription(userId);
  const [exposure] = await db
    .select()
    .from(exposures)
    .where(and(eq(exposures.id, exposureId), eq(exposures.subscriptionId, sub.id)))
    .limit(1);

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

  if (exposure.watchlistItemId) {
    const [item] = await db
      .select()
      .from(watchlistItems)
      .where(eq(watchlistItems.id, exposure.watchlistItemId))
      .limit(1);
    return { ...exposure, watchlistItem: item ?? null };
  }

  return { ...exposure, watchlistItem: null };
}

export async function checkTierLimits(userId: string): Promise<{ allowed: boolean; reason?: string }> {
  const sub = await getSubscription(userId);
  const tier = sub.tier;

  if (tier === "premium") {
    return { allowed: true };
  }

  const maxScans: Record<string, number> = {
    basic: 1,
    plus: 4,
  };

  const maxScanCount = maxScans[tier] ?? 1;
  const periodStart = tier === "plus"
    ? new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
    : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);

  const [result] = await db
    .select({ count: count() })
    .from(exposures)
    .where(
      and(
        eq(exposures.subscriptionId, sub.id),
        gte(exposures.detectedAt, periodStart),
      ),
    );

  if (result.count >= maxScanCount) {
    const periodLabel = tier === "plus" ? "week" : "month";
    return {
      allowed: false,
      reason: `Scan limit reached: ${maxScanCount} per ${periodLabel} for ${tier} tier`,
    };
  }

  return { allowed: true };
}

export async function runScan(userId: string): Promise<{ scanId: string }> {
  const sub = await getSubscription(userId);

  const tierCheck = await checkTierLimits(userId);
  if (!tierCheck.allowed) {
    throw new TRPCError({ code: "TOO_MANY_REQUESTS", message: tierCheck.reason });
  }

  if (scanStates.get(userId)?.status === "running") {
    throw new TRPCError({ code: "TOO_MANY_REQUESTS", message: "Scan already in progress" });
  }

  const scanId = crypto.randomUUID();
  scanStates.set(userId, {
    status: "running",
    startedAt: new Date(),
    completedAt: null,
    totalSources: 4,
    completedSources: 0,
    error: null,
  });

  const items = await db
    .select()
    .from(watchlistItems)
    .where(and(eq(watchlistItems.subscriptionId, sub.id), eq(watchlistItems.isActive, true)));

  processScan(userId, sub.id, items).catch((err) => {
    console.error("[darkwatch] Scan failed:", err);
    const state = scanStates.get(userId);
    if (state) {
      state.status = "failed";
      state.error = err instanceof Error ? err.message : "Unknown error";
    }
  });

  return { scanId };
}

async function processScan(
  userId: string,
  subscriptionId: string,
  items: Array<{ id: string; type: string; value: string }>,
): Promise<void> {
  const allResults: ScanResult[] = [];

  for (const item of items) {
    const sourcePromises: Promise<ScanResult[]>[] = [];

    switch (item.type) {
      case "email":
        sourcePromises.push(scanHIBP(item.value));
        break;
      case "domain":
        sourcePromises.push(scanSecurityTrails(item.value));
        sourcePromises.push(scanCensys(item.value));
        sourcePromises.push(scanShodan(item.value));
        break;
      case "phoneNumber":
        sourcePromises.push(scanShodan(item.value));
        sourcePromises.push(scanCensys(item.value));
        break;
      default:
        sourcePromises.push(scanShodan(item.value));
        break;
    }
    sourcePromises.push(scanForums(item.value));

    const results = await Promise.allSettled(sourcePromises);
    for (const r of results) {
      if (r.status === "fulfilled") {
        allResults.push(...r.value.map((sr) => ({ ...sr, watchlistItemId: item.id })));
      }
    }

    const state = scanStates.get(userId);
    if (state) {
      state.completedSources++;
    }
  }

  for (const result of allResults) {
    try {
      await processExposure({
        subscriptionId,
        watchlistItemId: (result as ScanResult & { watchlistItemId: string }).watchlistItemId,
        source: result.source,
        dataType: result.dataType,
        identifier: result.identifier,
        identifierHash: result.identifierHash,
        severity: result.severity,
        metadata: result.metadata,
        detectedAt: result.detectedAt,
      });
    } catch (err) {
      console.error("[darkwatch] Failed to process exposure:", err);
    }
  }

  const state = scanStates.get(userId);
  if (state) {
    state.status = "completed";
    state.completedAt = new Date();
  }
}

export async function getScanStatus(userId: string) {
  const state = scanStates.get(userId);
  if (!state) {
    return { status: "idle" as const, startedAt: null, completedAt: null, progress: 0 };
  }
  return {
    status: state.status,
    startedAt: state.startedAt,
    completedAt: state.completedAt,
    progress: state.totalSources > 0 ? state.completedSources / state.totalSources : 0,
    error: state.error,
  };
}

export async function getReports(
  userId: string,
  filters?: { page?: number; limit?: number },
) {
  const sub = await getSubscription(userId);
  const page = filters?.page ?? 1;
  const limit = filters?.limit ?? 20;
  const offset = (page - 1) * limit;

  const [totalResult] = await db
    .select({ count: count() })
    .from(securityReports)
    .where(eq(securityReports.subscriptionId, sub.id));

  const items = await db
    .select()
    .from(securityReports)
    .where(eq(securityReports.subscriptionId, sub.id))
    .orderBy(desc(securityReports.createdAt))
    .limit(limit)
    .offset(offset);

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