import crypto from "node:crypto";
import { Prisma } from "@prisma/client";
import { prisma } from "../../db/prisma.js";
import { enqueueSignalPush } from "../queues/push.queue.js";
import type { QueueSignalPayload, SignalsSnapshotJobData } from "../queues/queue.types.js";

function parseActiveTab(value: unknown) {
  if (value === null || value === undefined || value === "") return null;
  const parsed = Number(value);
  return Number.isInteger(parsed) ? parsed : null;
}

function normalizeDateValue(value: unknown) {
  if (!value) return null;
  const date = value instanceof Date ? value : new Date(value as string);
  const time = date.getTime();
  return Number.isNaN(time) ? null : new Date(time).toISOString();
}

function getMeaningfulRawPayload(rawPayload: QueueSignalPayload["rawPayload"]) {
  if (!rawPayload || typeof rawPayload !== "object" || Array.isArray(rawPayload)) {
    return rawPayload ?? null;
  }

  return {
    botKey: rawPayload.botKey || null,
    botName: rawPayload.botName || null,
    botUrl: rawPayload.botUrl || null,
    id: rawPayload.id || null,
    eventUrl: rawPayload.eventUrl || null,
    publicationType: rawPayload.publicationType || null,
    selectionText: rawPayload.selectionText || null,
    publicationTimer: rawPayload.publicationTimer || null,
    forecast: rawPayload.forecast || null,
    forecastRaw: rawPayload.forecastRaw || null,
    forecastImageUrl: rawPayload.forecastImageUrl || null,
    stake: rawPayload.stake || null,
    stakePercent: rawPayload.stakePercent || null,
    activeTab: parseActiveTab(rawPayload.activeTab),
    forecastInactive: rawPayload.forecastInactive === true,
    parserDetectedInactive: rawPayload.parserDetectedInactive === true
  };
}

function hasMeaningfulSignalChanges(existingSignal: {
  sportType: string;
  leagueName: string;
  homeTeam: string;
  awayTeam: string;
  forecast: string | null;
  eventStartTime: Date;
  odds: unknown;
  published: boolean;
  rawPayload: unknown;
} | null, nextSignal: QueueSignalPayload) {
  if (!existingSignal) return true;

  return (
    existingSignal.sportType !== nextSignal.sportType ||
    existingSignal.leagueName !== nextSignal.leagueName ||
    existingSignal.homeTeam !== nextSignal.homeTeam ||
    existingSignal.awayTeam !== nextSignal.awayTeam ||
    existingSignal.forecast !== nextSignal.forecast ||
    normalizeDateValue(existingSignal.eventStartTime) !== normalizeDateValue(nextSignal.eventStartTime) ||
    Number(existingSignal.odds) !== Number(nextSignal.odds) ||
    existingSignal.published !== nextSignal.published ||
    JSON.stringify(getMeaningfulRawPayload(existingSignal.rawPayload as QueueSignalPayload["rawPayload"])) !==
      JSON.stringify(getMeaningfulRawPayload(nextSignal.rawPayload))
  );
}

async function upsertSignalWithTransaction(
  tx: Prisma.TransactionClient,
  signal: QueueSignalPayload
) {
  const existingRows = await tx.$queryRawUnsafe<
    Array<{
      id: string;
      published: boolean;
      sportType: string;
      leagueName: string;
      homeTeam: string;
      awayTeam: string;
      forecast: string | null;
      eventStartTime: Date;
      odds: unknown;
      rawPayload: unknown;
    }>
  >(
    `
      SELECT
        id,
        "published",
        "sportType",
        "leagueName",
        "homeTeam",
        "awayTeam",
        "forecast",
        "eventStartTime",
        "odds",
        "rawPayload"
      FROM "Signal"
      WHERE "dedupeKey" = $1
      LIMIT 1
    `,
    signal.dedupeKey
  );

  const existingSignal = existingRows[0] || null;

  const result = await tx.$queryRawUnsafe<Array<{ id: string }>>(
    `
      INSERT INTO "Signal" (
        id, "providerId", "eventId", "sportType", "leagueName", "homeTeam", "awayTeam",
        "eventStartTime", "marketType", "selection", "forecast", "lineValue", "odds", "signalTime",
        "status", "sourceType", "comment", "published", "dedupeKey", "rawPayload",
        "createdAt", "updatedAt"
      )
      VALUES (
        $1, $2, $3, $4, $5, $6, $7,
        $8, $9, $10, $11, $12, $13, $14,
        $15::"SignalStatus", $16::"SourceType", $17, $18, $19, $20::jsonb,
        NOW(), NOW()
      )
      ON CONFLICT ("dedupeKey")
      DO UPDATE SET
        "sportType" = EXCLUDED."sportType",
        "leagueName" = EXCLUDED."leagueName",
        "homeTeam" = EXCLUDED."homeTeam",
        "awayTeam" = EXCLUDED."awayTeam",
        "eventStartTime" = EXCLUDED."eventStartTime",
        "forecast" = EXCLUDED."forecast",
        "odds" = EXCLUDED."odds",
        "signalTime" = CASE
          WHEN ABS(EXTRACT(EPOCH FROM ("Signal"."updatedAt" - "Signal"."signalTime"))) <= 120
            THEN EXCLUDED."signalTime"
          ELSE "Signal"."signalTime"
        END,
        "comment" = EXCLUDED."comment",
        "published" = true,
        "status" = 'pending'::"SignalStatus",
        "rawPayload" = EXCLUDED."rawPayload",
        "updatedAt" = NOW()
      RETURNING id
    `,
    existingSignal?.id || crypto.randomUUID(),
    signal.providerId,
    signal.eventId,
    signal.sportType,
    signal.leagueName,
    signal.homeTeam,
    signal.awayTeam,
    new Date(signal.eventStartTime),
    signal.marketType,
    signal.selection,
    signal.forecast,
    signal.lineValue,
    signal.odds,
    new Date(signal.signalTime),
    signal.status,
    signal.sourceType,
    signal.comment,
    signal.published,
    signal.dedupeKey,
    JSON.stringify(signal.rawPayload)
  );

  return {
    signalId: result[0]?.id || null,
    existed: Boolean(existingSignal),
    wasPublished: existingSignal?.published === true,
    changed: hasMeaningfulSignalChanges(existingSignal, signal)
  };
}

export async function syncParserSignalsSnapshot(job: SignalsSnapshotJobData) {
  const currentDedupeKeys = job.items.map((signal) => signal.dedupeKey);
  const changedSignalIds = await prisma.$transaction(async (tx) => {
    const collectedChangedSignalIds: string[] = [];

    for (const signal of job.items) {
      const result = await upsertSignalWithTransaction(tx, signal);
      if (result.signalId && (!result.existed || !result.wasPublished || result.changed)) {
        collectedChangedSignalIds.push(result.signalId);
      }
    }

    if (job.syncEligibleBotKeys.length > 0 && currentDedupeKeys.length > 0) {
      await tx.$executeRawUnsafe(
        `
          UPDATE "Signal"
          SET
            "published" = true,
            "status" = CASE
              WHEN "status" = 'unpublished'::"SignalStatus" THEN 'pending'::"SignalStatus"
              ELSE "status"
            END,
            "comment" = 'Signal marked inactive by parser',
            "rawPayload" = COALESCE("rawPayload", '{}'::jsonb) || '{"parserDetectedInactive":true,"forecastInactive":true,"activeTab":2}'::jsonb,
            "updatedAt" = NOW()
          WHERE "sourceType" = 'provider'::"SourceType"
            AND COALESCE("providerId", '') = $1
            AND COALESCE("rawPayload"->>'botKey', '') = ANY($2::text[])
            AND NOT ("dedupeKey" = ANY($3::text[]))
            AND (
              "published" = false
              OR COALESCE("rawPayload"->>'forecastInactive', 'false') <> 'true'
              OR COALESCE("rawPayload"->>'activeTab', '') <> '2'
              OR "status" = 'unpublished'::"SignalStatus"
            )
        `,
        job.providerId,
        job.syncEligibleBotKeys,
        currentDedupeKeys
      );
    } else if (job.syncEligibleBotKeys.length > 0) {
      await tx.$executeRawUnsafe(
        `
          UPDATE "Signal"
          SET
            "published" = true,
            "status" = CASE
              WHEN "status" = 'unpublished'::"SignalStatus" THEN 'pending'::"SignalStatus"
              ELSE "status"
            END,
            "comment" = 'Signal marked inactive by parser',
            "rawPayload" = COALESCE("rawPayload", '{}'::jsonb) || '{"parserDetectedInactive":true,"forecastInactive":true,"activeTab":2}'::jsonb,
            "updatedAt" = NOW()
          WHERE "sourceType" = 'provider'::"SourceType"
            AND COALESCE("providerId", '') = $1
            AND COALESCE("rawPayload"->>'botKey', '') = ANY($2::text[])
            AND (
              "published" = false
              OR COALESCE("rawPayload"->>'forecastInactive', 'false') <> 'true'
              OR COALESCE("rawPayload"->>'activeTab', '') <> '2'
              OR "status" = 'unpublished'::"SignalStatus"
            )
        `,
        job.providerId,
        job.syncEligibleBotKeys
      );
    }

    await tx.integrationLog.create({
      data: {
        provider: job.providerId,
        level: "info",
        message: "Parser snapshot synchronized from queue",
        payload: {
          meta: job.meta ?? null,
          itemsCount: job.items.length,
          syncEligibleBotKeys: job.syncEligibleBotKeys
        } as Prisma.InputJsonValue
      }
    });

    return collectedChangedSignalIds;
  });

  for (const signalId of changedSignalIds) {
    await enqueueSignalPush({
      type: "signal",
      signalId
    });
  }

  return {
    processed: job.items.length,
    changedSignalIds
  };
}
