/**
 * Billing Dunning routes — campaign management, retry, policy config.
 * @module dunning/routes
 */

import type { FastifyInstance } from 'fastify';
import {
  UnauthorizedError,
  ForbiddenError,
  NotFoundError,
  BadRequestError,
  ConflictError,
} from '../../lib/errors.js';
import { getRequestProductId } from '../../lib/request-context.js';
import {
  CreateDunningCampaignSchema,
  ResolveCampaignSchema,
  UpdateDunningPolicySchema,
  DEFAULT_RETRY_SCHEDULE_HOURS,
  DEFAULT_GRACE_PERIOD_DAYS,
  getNextRetryTime,
  type DunningCampaignDoc,
  type DunningPolicyDoc,
  type DunningStatus,
} from './types.js';
import * as repo from './repository.js';

function requireAuth(req: { jwtPayload?: { sub: string } }): string {
  if (!req.jwtPayload?.sub) throw new UnauthorizedError('Authentication required');
  return req.jwtPayload.sub;
}

function requireAdmin(req: { jwtPayload?: { sub: string; role?: string } }): string {
  const userId = requireAuth(req);
  if (req.jwtPayload?.role !== 'admin') throw new ForbiddenError('Admin access required');
  return userId;
}

export async function dunningRoutes(app: FastifyInstance): Promise<void> {
  // ── Create dunning campaign (triggered by Stripe webhook) ──
  app.post('/dunning/campaigns', async (req, reply) => {
    requireAdmin(req);
    const productId = getRequestProductId(req);
    const input = CreateDunningCampaignSchema.parse(req.body);

    // Check for existing active campaign for this subscription
    const existing = await repo.getActiveCampaignForUser(
      productId,
      input.userId,
      input.subscriptionId
    );
    if (existing) {
      throw new ConflictError(
        `Active dunning campaign already exists for subscription ${input.subscriptionId}: ${existing.id}`
      );
    }

    // Get policy for retry schedule
    const policy = await repo.getPolicy(productId);
    const retrySchedule = policy?.retryScheduleHours ?? DEFAULT_RETRY_SCHEDULE_HOURS;

    const now = new Date().toISOString();
    const doc: DunningCampaignDoc = {
      id: `dun_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
      productId,
      userId: input.userId,
      subscriptionId: input.subscriptionId,
      failedPaymentId: input.failedPaymentId,
      amountCents: input.amountCents,
      currency: input.currency,
      status: 'active',
      retryCount: 0,
      maxRetries: retrySchedule.length,
      nextRetryAt: getNextRetryTime(0, retrySchedule, now),
      gracePeriodEndsAt: null,
      actions: [
        {
          action: 'send_reminder',
          timestamp: now,
          success: true,
          details: 'Initial payment failure notification sent',
        },
      ],
      failureReason: input.failureReason,
      declineCode: input.declineCode,
      notificationsSent: 1,
      resolvedAt: null,
      createdAt: now,
      updatedAt: now,
    };

    const created = await repo.createCampaign(doc);
    req.log.info(
      { campaignId: created.id, userId: input.userId, amount: input.amountCents },
      'Dunning campaign created'
    );
    reply.status(201);
    return created;
  });

  // ── List campaigns ─────────────────────────────────────────
  app.get('/dunning/campaigns', async req => {
    requireAdmin(req);
    const productId = getRequestProductId(req);
    const {
      status,
      userId,
      limit: limitStr,
    } = req.query as {
      status?: string;
      userId?: string;
      limit?: string;
    };

    const parsedLimit = limitStr ? parseInt(limitStr, 10) : 50;
    const safeLimit =
      Number.isFinite(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, 200) : 50;

    return repo.listCampaigns(productId, {
      status: status as DunningStatus | undefined,
      userId,
      limit: safeLimit,
    });
  });

  // ── Get single campaign ────────────────────────────────────
  app.get<{ Params: { id: string } }>('/dunning/campaigns/:id', async req => {
    requireAdmin(req);
    const productId = getRequestProductId(req);
    const { id } = req.params;
    const campaign = await repo.getCampaign(id, productId);
    if (!campaign) throw new NotFoundError('Dunning campaign not found');
    return campaign;
  });

  // ── Manually resolve a campaign ────────────────────────────
  app.post<{ Params: { id: string } }>('/dunning/campaigns/:id/resolve', async req => {
    requireAdmin(req);
    const productId = getRequestProductId(req);
    const { id } = req.params;
    const input = ResolveCampaignSchema.parse(req.body);

    const campaign = await repo.getCampaign(id, productId);
    if (!campaign) throw new NotFoundError('Dunning campaign not found');

    if (campaign.status === 'resolved' || campaign.status === 'cancelled') {
      throw new BadRequestError(`Campaign already ${campaign.status}`);
    }

    const now = new Date().toISOString();
    const updated = await repo.updateCampaign(id, productId, {
      status: 'resolved',
      resolvedAt: now,
      actions: [
        ...campaign.actions,
        {
          action: 'retry_payment',
          timestamp: now,
          success: true,
          details: `Manually resolved: ${input.resolution}${input.notes ? ` — ${input.notes}` : ''}`,
        },
      ],
    });

    req.log.info({ campaignId: id, resolution: input.resolution }, 'Dunning campaign resolved');
    return updated;
  });

  // ── Retry payment for a campaign ───────────────────────────
  app.post<{ Params: { id: string } }>('/dunning/campaigns/:id/retry', async req => {
    requireAdmin(req);
    const productId = getRequestProductId(req);
    const { id } = req.params;

    const campaign = await repo.getCampaign(id, productId);
    if (!campaign) throw new NotFoundError('Dunning campaign not found');

    if (campaign.status !== 'active' && campaign.status !== 'grace_period') {
      throw new BadRequestError(`Cannot retry: campaign status is ${campaign.status}`);
    }

    // Get policy
    const policy = await repo.getPolicy(productId);
    const retrySchedule = policy?.retryScheduleHours ?? DEFAULT_RETRY_SCHEDULE_HOURS;
    const gracePeriodDays = policy?.gracePeriodDays ?? DEFAULT_GRACE_PERIOD_DAYS;

    const now = new Date().toISOString();
    const newRetryCount = campaign.retryCount + 1;

    // In production, this would call Stripe to retry the payment.
    // For MVP, simulate the retry as failed and advance the schedule.
    const retrySuccess = false;

    if (retrySuccess) {
      await repo.updateCampaign(id, productId, {
        status: 'resolved',
        resolvedAt: now,
        retryCount: newRetryCount,
        nextRetryAt: null,
        actions: [
          ...campaign.actions,
          {
            action: 'retry_payment',
            timestamp: now,
            success: true,
            details: 'Payment retry succeeded',
          },
        ],
      });
      return { status: 'resolved', message: 'Payment retry succeeded' };
    }

    // Retry failed — check if we've exhausted retries
    if (newRetryCount >= retrySchedule.length) {
      // Enter grace period
      const gracePeriodEnd = new Date();
      gracePeriodEnd.setDate(gracePeriodEnd.getDate() + gracePeriodDays);

      await repo.updateCampaign(id, productId, {
        status: 'grace_period',
        retryCount: newRetryCount,
        nextRetryAt: null,
        gracePeriodEndsAt: gracePeriodEnd.toISOString(),
        notificationsSent: campaign.notificationsSent + 1,
        actions: [
          ...campaign.actions,
          {
            action: 'retry_payment',
            timestamp: now,
            success: false,
            details: `Retry ${newRetryCount} failed`,
          },
          {
            action: 'send_warning',
            timestamp: now,
            success: true,
            details: `Grace period started (${gracePeriodDays} days)`,
          },
        ],
      });

      return {
        status: 'grace_period',
        message: `All ${retrySchedule.length} retries exhausted. Grace period of ${gracePeriodDays} days started.`,
        gracePeriodEndsAt: gracePeriodEnd.toISOString(),
      };
    }

    // Schedule next retry
    const nextRetry = getNextRetryTime(newRetryCount, retrySchedule, campaign.createdAt);
    await repo.updateCampaign(id, productId, {
      retryCount: newRetryCount,
      nextRetryAt: nextRetry,
      notificationsSent: campaign.notificationsSent + 1,
      actions: [
        ...campaign.actions,
        {
          action: 'retry_payment',
          timestamp: now,
          success: false,
          details: `Retry ${newRetryCount} failed`,
        },
        {
          action: 'send_reminder',
          timestamp: now,
          success: true,
          details: `Payment reminder sent (attempt ${newRetryCount + 1} scheduled)`,
        },
      ],
    });

    return {
      status: 'active',
      message: `Retry ${newRetryCount} failed. Next retry scheduled.`,
      nextRetryAt: nextRetry,
    };
  });

  // ── Process expired grace periods ──────────────────────────
  app.post('/dunning/process-expired', async req => {
    requireAdmin(req);
    const productId = getRequestProductId(req);

    const expired = await repo.getCampaignsWithExpiredGrace(productId);
    const policy = await repo.getPolicy(productId);
    const autoDowngrade = policy?.autoDowngrade ?? true;
    const now = new Date().toISOString();

    const results: Array<{ campaignId: string; action: string }> = [];

    for (const campaign of expired) {
      const newStatus: DunningStatus = autoDowngrade ? 'downgraded' : 'cancelled';
      const actionType = autoDowngrade ? 'downgrade' : 'cancel';

      await repo.updateCampaign(campaign.id, productId, {
        status: newStatus,
        actions: [
          ...campaign.actions,
          {
            action: actionType,
            timestamp: now,
            success: true,
            details: autoDowngrade
              ? `Subscription downgraded to ${policy?.downgradeToPlan ?? 'free'} after grace period expired`
              : 'Subscription cancelled after grace period expired',
          },
        ],
      });

      results.push({ campaignId: campaign.id, action: newStatus });
    }

    req.log.info({ productId, processedCount: results.length }, 'Processed expired grace periods');
    return { processed: results.length, results };
  });

  // ── Dunning stats ──────────────────────────────────────────
  app.get('/dunning/stats', async req => {
    requireAdmin(req);
    const productId = getRequestProductId(req);
    return repo.getDunningStats(productId);
  });

  // ── Get dunning policy ─────────────────────────────────────
  app.get('/dunning/policy', async req => {
    requireAdmin(req);
    const productId = getRequestProductId(req);
    const policy = await repo.getPolicy(productId);

    if (!policy) {
      // Return defaults
      return {
        productId,
        retryScheduleHours: DEFAULT_RETRY_SCHEDULE_HOURS,
        gracePeriodDays: DEFAULT_GRACE_PERIOD_DAYS,
        autoDowngrade: true,
        downgradeToPlan: 'free',
        reminderTemplates: ['payment_failed', 'payment_retry', 'grace_period_warning'],
        sendFinalWarning: true,
        finalWarningDaysBeforeEnd: 2,
      };
    }

    return policy;
  });

  // ── Update dunning policy ──────────────────────────────────
  app.put('/dunning/policy', async req => {
    const userId = requireAdmin(req);
    const productId = getRequestProductId(req);
    const input = UpdateDunningPolicySchema.parse(req.body);

    const existing = await repo.getPolicy(productId);
    const now = new Date().toISOString();

    const doc: DunningPolicyDoc = {
      id: productId,
      productId,
      retryScheduleHours:
        input.retryScheduleHours ?? existing?.retryScheduleHours ?? DEFAULT_RETRY_SCHEDULE_HOURS,
      gracePeriodDays:
        input.gracePeriodDays ?? existing?.gracePeriodDays ?? DEFAULT_GRACE_PERIOD_DAYS,
      autoDowngrade: input.autoDowngrade ?? existing?.autoDowngrade ?? true,
      downgradeToPlan: input.downgradeToPlan ?? existing?.downgradeToPlan ?? 'free',
      reminderTemplates: input.reminderTemplates ??
        existing?.reminderTemplates ?? ['payment_failed', 'payment_retry', 'grace_period_warning'],
      sendFinalWarning: input.sendFinalWarning ?? existing?.sendFinalWarning ?? true,
      finalWarningDaysBeforeEnd:
        input.finalWarningDaysBeforeEnd ?? existing?.finalWarningDaysBeforeEnd ?? 2,
      updatedAt: now,
      updatedBy: userId,
    };

    const saved = await repo.upsertPolicy(doc);
    req.log.info({ productId, userId }, 'Dunning policy updated');
    return saved;
  });
}
