import { Prisma, ActionStepKind, ActionStepCompletionSource } from '@prisma/client';
import { prisma } from '../../config/database';
import { AppError } from '../../middleware/error-handler';
import type {
  CreateActionCampaignInput,
  UpdateActionCampaignInput,
  CreateActionStepInput,
  UpdateActionStepInput,
} from './action-campaigns.schemas';

function generateSlug(title: string): string {
  return title
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .slice(0, 80);
}

async function resolveSlugCollision(slug: string, excludeId?: string): Promise<string> {
  let candidate = slug;
  let suffix = 2;
  while (true) {
    const existing = await prisma.actionCampaign.findUnique({
      where: { slug: candidate },
      select: { id: true },
    });
    if (!existing || (excludeId && existing.id === excludeId)) return candidate;
    candidate = `${slug}-${suffix}`;
    suffix++;
  }
}

const campaignWithSteps = {
  include: {
    steps: { orderBy: { order: 'asc' as const } },
  },
} satisfies Prisma.ActionCampaignDefaultArgs;

type CampaignWithSteps = Prisma.ActionCampaignGetPayload<typeof campaignWithSteps>;

export interface ActiveCampaignStepForUser {
  id: string;
  order: number;
  kind: ActionStepKind;
  label: string;
  description: string | null;
  targetId: string | null;
  targetUrl: string | null;
  autoComplete: boolean;
  completed: boolean;
  completedAt: Date | null;
  source: ActionStepCompletionSource | null;
}

export interface ActiveCampaignForUser {
  id: string;
  slug: string;
  title: string;
  description: string | null;
  rewardText: string | null;
  isActive: boolean;
  startsAt: Date | null;
  endsAt: Date | null;
  minStepsForReward: number | null;
  totalSteps: number;
  completedSteps: number;
  rewardEarned: boolean;
  steps: ActiveCampaignStepForUser[];
}

async function isStepAutoCompleted(
  userId: string,
  userEmail: string | null,
  step: CampaignWithSteps['steps'][number],
): Promise<boolean> {
  if (!step.targetId) return false;
  switch (step.kind) {
    case ActionStepKind.WATCH_VIDEO: {
      const videoId = Number.parseInt(step.targetId, 10);
      if (!Number.isFinite(videoId)) return false;
      const view = await prisma.videoView.findFirst({
        where: { userId, videoId },
        select: { id: true },
      });
      return !!view;
    }
    case ActionStepKind.SUBMIT_INFLUENCE: {
      const email = await prisma.campaignEmail.findFirst({
        where: { userId, campaignSlug: step.targetId },
        select: { id: true },
      });
      return !!email;
    }
    case ActionStepKind.SIGN_PETITION: {
      if (!userEmail) return false;
      const signature = await prisma.petitionSignature.findFirst({
        where: {
          signerEmail: userEmail,
          petition: { slug: step.targetId },
        },
        select: { id: true },
      });
      return !!signature;
    }
    case ActionStepKind.RSVP_EVENT: {
      const ticket = await prisma.ticket.findFirst({
        where: {
          userId,
          event: { slug: step.targetId },
        },
        select: { id: true },
      });
      return !!ticket;
    }
    case ActionStepKind.SIGNUP_SHIFT: {
      const signup = await prisma.shiftSignup.findFirst({
        where: { userId, shiftId: step.targetId },
        select: { id: true },
      });
      return !!signup;
    }
    case ActionStepKind.JOIN_CHALLENGE: {
      const member = await prisma.challengeTeamMember.findFirst({
        where: {
          userId,
          team: { challengeId: step.targetId },
        },
        select: { id: true },
      });
      return !!member;
    }
    case ActionStepKind.VISIT_LINK:
    case ActionStepKind.CUSTOM:
      return false;
    default:
      return false;
  }
}

export const actionCampaignsService = {
  async listCampaigns() {
    return prisma.actionCampaign.findMany({
      orderBy: [{ isActive: 'desc' }, { createdAt: 'desc' }],
      include: {
        steps: { orderBy: { order: 'asc' } },
        _count: { select: { steps: true } },
      },
    });
  },

  async getCampaign(idOrSlug: string): Promise<CampaignWithSteps> {
    const campaign = await prisma.actionCampaign.findFirst({
      where: { OR: [{ id: idOrSlug }, { slug: idOrSlug }] },
      include: { steps: { orderBy: { order: 'asc' } } },
    });
    if (!campaign) throw new AppError(404, 'Action campaign not found', 'ACTION_CAMPAIGN_NOT_FOUND');
    return campaign;
  },

  async createCampaign(data: CreateActionCampaignInput, createdByUserId: string) {
    const slug = await resolveSlugCollision(data.slug ? generateSlug(data.slug) : generateSlug(data.title));
    return prisma.actionCampaign.create({
      data: {
        slug,
        title: data.title,
        description: data.description ?? null,
        rewardText: data.rewardText ?? null,
        isActive: data.isActive ?? false,
        startsAt: data.startsAt ?? null,
        endsAt: data.endsAt ?? null,
        minStepsForReward: data.minStepsForReward ?? null,
        createdByUserId,
      },
      include: { steps: { orderBy: { order: 'asc' } } },
    });
  },

  async updateCampaign(id: string, data: UpdateActionCampaignInput) {
    const existing = await prisma.actionCampaign.findUnique({ where: { id }, select: { id: true, slug: true } });
    if (!existing) throw new AppError(404, 'Action campaign not found', 'ACTION_CAMPAIGN_NOT_FOUND');

    let slug: string | undefined;
    if (data.slug && data.slug !== existing.slug) {
      slug = await resolveSlugCollision(generateSlug(data.slug), id);
    }

    return prisma.actionCampaign.update({
      where: { id },
      data: {
        ...(data.title !== undefined && { title: data.title }),
        ...(data.description !== undefined && { description: data.description }),
        ...(data.rewardText !== undefined && { rewardText: data.rewardText }),
        ...(data.isActive !== undefined && { isActive: data.isActive }),
        ...(data.startsAt !== undefined && { startsAt: data.startsAt }),
        ...(data.endsAt !== undefined && { endsAt: data.endsAt }),
        ...(data.minStepsForReward !== undefined && { minStepsForReward: data.minStepsForReward }),
        ...(slug && { slug }),
      },
      include: { steps: { orderBy: { order: 'asc' } } },
    });
  },

  async deleteCampaign(id: string) {
    const existing = await prisma.actionCampaign.findUnique({ where: { id }, select: { id: true } });
    if (!existing) throw new AppError(404, 'Action campaign not found', 'ACTION_CAMPAIGN_NOT_FOUND');
    await prisma.actionCampaign.delete({ where: { id } });
  },

  async activateCampaign(id: string) {
    const existing = await prisma.actionCampaign.findUnique({ where: { id }, select: { id: true } });
    if (!existing) throw new AppError(404, 'Action campaign not found', 'ACTION_CAMPAIGN_NOT_FOUND');

    return prisma.$transaction(async (tx) => {
      await tx.actionCampaign.updateMany({
        where: { id: { not: id }, isActive: true },
        data: { isActive: false },
      });
      return tx.actionCampaign.update({
        where: { id },
        data: { isActive: true },
        include: { steps: { orderBy: { order: 'asc' } } },
      });
    });
  },

  async addStep(campaignId: string, data: CreateActionStepInput) {
    const campaign = await prisma.actionCampaign.findUnique({
      where: { id: campaignId },
      select: { id: true },
    });
    if (!campaign) throw new AppError(404, 'Action campaign not found', 'ACTION_CAMPAIGN_NOT_FOUND');

    const last = await prisma.actionStep.findFirst({
      where: { campaignId },
      orderBy: { order: 'desc' },
      select: { order: true },
    });
    const nextOrder = (last?.order ?? -1) + 1;

    return prisma.actionStep.create({
      data: {
        campaignId,
        order: nextOrder,
        kind: data.kind,
        label: data.label,
        description: data.description ?? null,
        targetId: data.targetId ?? null,
        targetUrl: data.targetUrl ?? null,
        autoComplete: data.autoComplete ?? true,
      },
    });
  },

  async updateStep(stepId: string, data: UpdateActionStepInput) {
    const existing = await prisma.actionStep.findUnique({ where: { id: stepId }, select: { id: true } });
    if (!existing) throw new AppError(404, 'Action step not found', 'ACTION_STEP_NOT_FOUND');

    return prisma.actionStep.update({
      where: { id: stepId },
      data: {
        ...(data.kind !== undefined && { kind: data.kind }),
        ...(data.label !== undefined && { label: data.label }),
        ...(data.description !== undefined && { description: data.description }),
        ...(data.targetId !== undefined && { targetId: data.targetId }),
        ...(data.targetUrl !== undefined && { targetUrl: data.targetUrl }),
        ...(data.autoComplete !== undefined && { autoComplete: data.autoComplete }),
      },
    });
  },

  async deleteStep(stepId: string) {
    const existing = await prisma.actionStep.findUnique({ where: { id: stepId }, select: { id: true } });
    if (!existing) throw new AppError(404, 'Action step not found', 'ACTION_STEP_NOT_FOUND');
    await prisma.actionStep.delete({ where: { id: stepId } });
  },

  async reorderSteps(campaignId: string, stepIdOrder: string[]) {
    const existing = await prisma.actionStep.findMany({
      where: { campaignId },
      select: { id: true },
    });
    const existingIds = new Set(existing.map((s) => s.id));
    if (stepIdOrder.length !== existing.length || !stepIdOrder.every((id) => existingIds.has(id))) {
      throw new AppError(400, 'stepIds must match existing steps for this campaign', 'INVALID_REORDER');
    }

    // Two-phase update to avoid @@unique([campaignId, order]) collisions
    await prisma.$transaction(async (tx) => {
      const offset = existing.length;
      for (let i = 0; i < stepIdOrder.length; i++) {
        await tx.actionStep.update({
          where: { id: stepIdOrder[i]! },
          data: { order: i + offset },
        });
      }
      for (let i = 0; i < stepIdOrder.length; i++) {
        await tx.actionStep.update({
          where: { id: stepIdOrder[i]! },
          data: { order: i },
        });
      }
    });

    return prisma.actionStep.findMany({
      where: { campaignId },
      orderBy: { order: 'asc' },
    });
  },

  async getActiveCampaign(): Promise<CampaignWithSteps | null> {
    const campaign = await prisma.actionCampaign.findFirst({
      where: { isActive: true },
      orderBy: { createdAt: 'desc' },
      include: { steps: { orderBy: { order: 'asc' } } },
    });
    return campaign;
  },

  async getActiveForUser(userId: string): Promise<ActiveCampaignForUser | null> {
    const campaign = await this.getActiveCampaign();
    if (!campaign) return null;

    const user = await prisma.user.findUnique({
      where: { id: userId },
      select: { email: true },
    });
    const userEmail = user?.email ?? null;

    const completions = await prisma.actionStepCompletion.findMany({
      where: { userId, stepId: { in: campaign.steps.map((s) => s.id) } },
      select: { stepId: true, completedAt: true, source: true },
    });
    const completionByStepId = new Map(completions.map((c) => [c.stepId, c]));

    const stepResults: ActiveCampaignStepForUser[] = [];
    for (const step of campaign.steps) {
      const existing = completionByStepId.get(step.id);
      let completed = !!existing;
      let completedAt: Date | null = existing?.completedAt ?? null;
      let source: ActionStepCompletionSource | null = existing?.source ?? null;

      if (!completed && step.autoComplete) {
        const auto = await isStepAutoCompleted(userId, userEmail, step);
        if (auto) {
          completed = true;
          completedAt = new Date();
          source = ActionStepCompletionSource.AUTO;
        }
      }

      stepResults.push({
        id: step.id,
        order: step.order,
        kind: step.kind,
        label: step.label,
        description: step.description,
        targetId: step.targetId,
        targetUrl: step.targetUrl,
        autoComplete: step.autoComplete,
        completed,
        completedAt,
        source,
      });
    }

    const totalSteps = stepResults.length;
    const completedSteps = stepResults.filter((s) => s.completed).length;
    const requiredSteps = campaign.minStepsForReward ?? totalSteps;
    const rewardEarned = totalSteps > 0 && completedSteps >= requiredSteps;

    return {
      id: campaign.id,
      slug: campaign.slug,
      title: campaign.title,
      description: campaign.description,
      rewardText: campaign.rewardText,
      isActive: campaign.isActive,
      startsAt: campaign.startsAt,
      endsAt: campaign.endsAt,
      minStepsForReward: campaign.minStepsForReward,
      totalSteps,
      completedSteps,
      rewardEarned,
      steps: stepResults,
    };
  },

  async markStepComplete(
    userId: string,
    stepId: string,
    source: ActionStepCompletionSource = ActionStepCompletionSource.SELF_REPORTED,
  ) {
    const step = await prisma.actionStep.findUnique({
      where: { id: stepId },
      include: { campaign: { select: { id: true, isActive: true } } },
    });
    if (!step) throw new AppError(404, 'Action step not found', 'ACTION_STEP_NOT_FOUND');

    const existing = await prisma.actionStepCompletion.findUnique({
      where: { userId_stepId: { userId, stepId } },
    });
    if (existing) return existing;

    const isSelfReportable =
      step.kind === ActionStepKind.CUSTOM || step.kind === ActionStepKind.VISIT_LINK;

    if (!isSelfReportable) {
      const user = await prisma.user.findUnique({ where: { id: userId }, select: { email: true } });
      const autoOk = await isStepAutoCompleted(userId, user?.email ?? null, step);
      if (!autoOk) {
        throw new AppError(
          400,
          'Step cannot be self-reported without completing the required action',
          'ACTION_STEP_NOT_COMPLETED',
        );
      }
      return prisma.actionStepCompletion.create({
        data: { userId, stepId, source: ActionStepCompletionSource.AUTO },
      });
    }

    return prisma.actionStepCompletion.create({
      data: { userId, stepId, source },
    });
  },
};
