import type Stripe from 'stripe';
import { and, eq } from 'drizzle-orm';
import { z } from 'zod';
import {
  affiliateEnrollments,
  affiliateEntriesTable as affiliateEntries,
  affiliatePayouts,
  kycVerifications,
  ledgerEntries,
  referralLinks,
} from '@/server/db/schema';
import type { DrizzleClient, TxDrizzleClient } from '@/server/db/client';
import { asPayoutId } from '@/server/platform-seams/ids';
import { insertFraudEvent } from '@/server/db/queries/fraud-events';
import {
  createAffiliateAccountLink,
  createAffiliateConnectAccount,
  syncAffiliateStripeStatus,
} from '@/server/payments/connect/affiliate-onboarding';
import { runAffiliatePayout } from '@/server/payments/connect/affiliate-payout';
import { bindReferralOnSignup } from '@/server/referrals/attribution';
import { applyClawback } from '@/server/referrals/clawback';
import { FactoryOwnershipError, type FactoryOperationHandler, type FactoryStore } from './core';

const uuid = z.uuid();
const attributeReferralInput = z.object({
  affiliateUserId: uuid,
  refereeUserId: uuid,
  linkId: uuid,
});
const affiliateUserInput = z.object({ userId: uuid });
const fraudEventInput = z.object({
  userId: uuid,
  decisionPoint: z.literal('SIGNUP'),
  action: z.literal('flag'),
});
const clawbackInput = z.object({
  userId: uuid,
  originalEntryId: uuid,
  amountAgorot: z.number().int().positive(),
});
const settlePayoutInput = z.object({
  userId: uuid,
  payoutId: uuid,
  scenario: z.enum(['success', 'transferFailure', 'payoutFailure']).default('success'),
});

async function requireOwnedActor(store: FactoryStore, runId: string, userId: string) {
  if (!(await store.findOwned(runId, 'actor', userId))) {
    throw new FactoryOwnershipError(`actor ${userId} is not owned by run`);
  }
}

function unsupportedRead(): never {
  throw new Error('Affiliate transition operations do not expose entity readback');
}

function unsupportedCleanup(): never {
  throw new Error('Affiliate transition operations are cleaned through their owned actors');
}

export function createAffiliateFactoryHandlers(input: {
  db: DrizzleClient;
  store: FactoryStore;
}): Record<string, FactoryOperationHandler> {
  const { db, store } = input;
  return {
    attributeAffiliateReferral: {
      dependencyOrder: 25,
      async execute({ runId, input: raw }) {
        const parsed = attributeReferralInput.parse(raw);
        await requireOwnedActor(store, runId, parsed.affiliateUserId);
        await requireOwnedActor(store, runId, parsed.refereeUserId);
        const [link] = await db
          .select({ id: referralLinks.id, ownerUserId: referralLinks.ownerUserId })
          .from(referralLinks)
          .where(
            and(
              eq(referralLinks.id, parsed.linkId),
              eq(referralLinks.ownerUserId, parsed.affiliateUserId),
              eq(referralLinks.kind, 'affiliate'),
              eq(referralLinks.active, true),
            ),
          )
          .limit(1);
        if (!link) throw new FactoryOwnershipError('active affiliate link is not owned by actor');
        const referral = await bindReferralOnSignup(db, {
          refereeUserId: parsed.refereeUserId,
          linkId: parsed.linkId,
          clickedAt: new Date(),
          visitorId: `factory-${runId}`,
        });
        if (!referral || referral.referrerUserId !== parsed.affiliateUserId) {
          throw new Error('Referral attribution was rejected');
        }
        return {
          result: {
            referralId: referral.id,
            status: referral.status,
            referrerUserId: referral.referrerUserId,
            refereeUserId: referral.refereeUserId,
          },
        };
      },
      read: unsupportedRead,
      cleanup: unsupportedCleanup,
    },

    startAffiliateConnectOnboarding: {
      dependencyOrder: 25,
      async execute({ runId, input: raw }) {
        const { userId } = affiliateUserInput.parse(raw);
        await requireOwnedActor(store, runId, userId);
        const [enrollment] = await db
          .select()
          .from(affiliateEnrollments)
          .where(
            and(eq(affiliateEnrollments.userId, userId), eq(affiliateEnrollments.status, 'active')),
          )
          .limit(1);
        if (!enrollment) throw new Error('Active affiliate enrollment required');

        let accountCreates = 0;
        let accountLinkCreates = 0;
        const accountId =
          enrollment.stripeAccountId ?? `acct_factory_${userId.replaceAll('-', '')}`;
        const stripe = {
          accounts: {
            create: async () => {
              accountCreates += 1;
              return { id: accountId };
            },
          },
          accountLinks: {
            create: async () => {
              accountLinkCreates += 1;
              return { url: `https://connect.stripe.com/setup/${accountId}` };
            },
          },
        } as unknown as Stripe;

        if (!enrollment.stripeAccountId) {
          await createAffiliateConnectAccount(
            stripe,
            db,
            enrollment.id,
            `affiliate+${userId}@multideal.test`,
          );
        }
        const link = await createAffiliateAccountLink(stripe, accountId, {
          returnUrl: 'https://multi.deal/affiliate/connect/return',
          refreshUrl: 'https://multi.deal/affiliate/connect/refresh',
        });
        return {
          result: {
            accountId,
            url: link.url,
            providerCalls: { accountCreates, accountLinkCreates },
          },
        };
      },
      read: unsupportedRead,
      cleanup: unsupportedCleanup,
    },

    completeAffiliatePayoutConnection: {
      dependencyOrder: 25,
      async execute({ runId, input: raw }) {
        const { userId } = affiliateUserInput.parse(raw);
        await requireOwnedActor(store, runId, userId);
        const [enrollment] = await db
          .select({ stripeAccountId: affiliateEnrollments.stripeAccountId })
          .from(affiliateEnrollments)
          .where(eq(affiliateEnrollments.userId, userId))
          .limit(1);
        if (!enrollment?.stripeAccountId) throw new Error('Affiliate Connect account required');
        await syncAffiliateStripeStatus(db, enrollment.stripeAccountId, {
          detailsSubmitted: true,
          chargesEnabled: false,
          payoutsEnabled: true,
        });
        await db
          .insert(kycVerifications)
          .values({ userId, status: 'verified', submittedAt: new Date(), verifiedAt: new Date() })
          .onConflictDoUpdate({
            target: kycVerifications.userId,
            set: { status: 'verified', verifiedAt: new Date() },
          });
        return { result: { accountId: enrollment.stripeAccountId, webhookEvents: 1 } };
      },
      read: unsupportedRead,
      cleanup: unsupportedCleanup,
    },

    verifyAffiliateKyc: {
      dependencyOrder: 25,
      async execute({ runId, input: raw }) {
        const { userId } = affiliateUserInput.parse(raw);
        await requireOwnedActor(store, runId, userId);
        await db
          .insert(kycVerifications)
          .values({ userId, status: 'verified', submittedAt: new Date(), verifiedAt: new Date() })
          .onConflictDoUpdate({
            target: kycVerifications.userId,
            set: { status: 'verified', verifiedAt: new Date() },
          });
        return { result: { status: 'verified' } };
      },
      read: unsupportedRead,
      cleanup: unsupportedCleanup,
    },

    createAffiliateFraudEvent: {
      dependencyOrder: 25,
      async execute({ runId, input: raw }) {
        const parsed = fraudEventInput.parse(raw);
        await requireOwnedActor(store, runId, parsed.userId);
        const code = `factory-ui-flag-${runId}`;
        const eventId = await insertFraudEvent(db, {
          userId: parsed.userId,
          decisionPoint: parsed.decisionPoint,
          signal: {
            adapter: 'factory-ui-fraud-event',
            action: parsed.action,
            codes: [code],
          },
        });
        if (!eventId) throw new Error('Open fraud event already exists');
        return { result: { eventId, code } };
      },
      read: unsupportedRead,
      cleanup: unsupportedCleanup,
    },

    applyAffiliateFraudClawback: {
      dependencyOrder: 35,
      async execute({ runId, input: raw }) {
        const parsed = clawbackInput.parse(raw);
        await requireOwnedActor(store, runId, parsed.userId);
        const [entry] = await db
          .select({
            ownerId: affiliateEntries.ownerId,
            entryType: affiliateEntries.entryType,
            sourceType: affiliateEntries.sourceType,
            referralId: affiliateEntries.referralId,
            amountAgorot: ledgerEntries.delta,
          })
          .from(affiliateEntries)
          .innerJoin(ledgerEntries, eq(ledgerEntries.id, affiliateEntries.entryId))
          .where(eq(affiliateEntries.entryId, parsed.originalEntryId))
          .limit(1);
        if (
          !entry ||
          entry.ownerId !== parsed.userId ||
          !['affiliate_commission', 'referral_reward'].includes(entry.entryType) ||
          entry.sourceType === 'journey_e2e' ||
          Number(entry.amountAgorot) <= 0
        ) {
          throw new FactoryOwnershipError('real affiliate earning is not owned by actor');
        }
        const fraudEventId = await insertFraudEvent(db, {
          userId: parsed.userId,
          ...(entry.referralId ? { referralId: entry.referralId } : {}),
          decisionPoint: 'EARN',
          signal: {
            adapter: 'factory-confirmed-fraud',
            action: 'block',
            codes: ['confirmed_fraud'],
          },
        });
        if (!fraudEventId) throw new Error('Open fraud event already exists');
        const clawback = await applyClawback(
          db as TxDrizzleClient,
          parsed.originalEntryId,
          parsed.amountAgorot,
          fraudEventId,
        );
        return { result: { ...clawback, fraudEventId } };
      },
      read: unsupportedRead,
      cleanup: unsupportedCleanup,
    },

    settleAffiliatePayout: {
      dependencyOrder: 35,
      async execute({ runId, input: raw }) {
        const parsed = settlePayoutInput.parse(raw);
        await requireOwnedActor(store, runId, parsed.userId);
        const [payout] = await db
          .select({
            id: affiliatePayouts.id,
            status: affiliatePayouts.status,
            amountAgorot: affiliatePayouts.amountAgorot,
            enrollmentId: affiliatePayouts.enrollmentId,
            stripeAccountId: affiliateEnrollments.stripeAccountId,
            stripePayoutsEnabled: affiliateEnrollments.stripePayoutsEnabled,
          })
          .from(affiliatePayouts)
          .innerJoin(
            affiliateEnrollments,
            eq(affiliateEnrollments.id, affiliatePayouts.enrollmentId),
          )
          .where(
            and(
              eq(affiliatePayouts.id, asPayoutId(parsed.payoutId)),
              eq(affiliatePayouts.userId, parsed.userId),
            ),
          )
          .limit(1);
        if (!payout?.stripeAccountId) throw new Error('Owned connected payout required');
        if (!payout.stripePayoutsEnabled) {
          throw new Error('Affiliate Stripe payouts must be enabled before factory settlement');
        }
        if (payout.status === 'requested') {
          await db
            .update(affiliatePayouts)
            .set({
              status: 'approved',
              approvedAt: new Date(),
              approvedByUserId: parsed.userId,
            })
            .where(eq(affiliatePayouts.id, payout.id));
        } else if (payout.status !== 'approved') {
          throw new Error(
            `Factory payout settlement requires requested or approved status, got ${payout.status}`,
          );
        }
        let transferCreates = 0;
        let payoutCreates = 0;
        const stripe = {
          transfers: {
            create: async () => {
              transferCreates += 1;
              if (parsed.scenario === 'transferFailure')
                throw new Error('factory transfer failure');
              return { id: `tr_factory_${parsed.payoutId.replaceAll('-', '')}` };
            },
          },
          payouts: {
            create: async () => {
              payoutCreates += 1;
              if (parsed.scenario === 'payoutFailure') throw new Error('factory payout failure');
              return { id: `po_factory_${parsed.payoutId.replaceAll('-', '')}` };
            },
          },
        } as unknown as Stripe;
        const result = await runAffiliatePayout(stripe, db as TxDrizzleClient, payout.id, {
          amountAgorot: Number(payout.amountAgorot),
          stripeAccountId: payout.stripeAccountId,
          enrollmentId: payout.enrollmentId,
          stripePayoutsEnabled: payout.stripePayoutsEnabled,
        });
        return { result: { ...result, providerCalls: { transferCreates, payoutCreates } } };
      },
      read: unsupportedRead,
      cleanup: unsupportedCleanup,
    },
  };
}
