import crypto from 'crypto';
import { Prisma, StrawPollStatus, StrawPollType } from '@prisma/client';
import { prisma } from '../../config/database';
import { logger } from '../../utils/logger';
import { generateSlug } from '../../utils/slug';
import { pollSseService } from './polls-sse.service';
import type {
  CreateStrawPollInput,
  UpdateStrawPollInput,
  SubmitStrawPollVoteInput,
  SubmitStrawPollCommentInput,
  ListStrawPollsInput,
} from './polls.schemas';

function generateVoterToken(): string {
  return crypto.randomBytes(18).toString('base64url').slice(0, 24);
}

// Select configs for different contexts
const pollListSelect = {
  id: true,
  slug: true,
  title: true,
  type: true,
  status: true,
  identityMode: true,
  resultVisibility: true,
  isPrivate: true,
  closesAt: true,
  closeThreshold: true,
  allowComments: true,
  createdByUserId: true,
  createdAt: true,
  updatedAt: true,
  _count: { select: { votes: true, comments: true, options: true } },
} satisfies Prisma.StrawPollSelect;

const pollDetailSelect = {
  ...pollListSelect,
  description: true,
  autoCloseJobId: true,
  options: { orderBy: { sortOrder: 'asc' as const }, include: { _count: { select: { votes: true } } } },
  votes: {
    select: {
      id: true, optionId: true, userId: true, voterName: true, createdAt: true,
      user: { select: { id: true, name: true, email: true } },
    },
    orderBy: { createdAt: 'desc' as const },
  },
  comments: {
    select: { id: true, authorName: true, content: true, userId: true, createdAt: true },
    orderBy: { createdAt: 'desc' as const },
  },
  createdBy: { select: { id: true, name: true, email: true } },
};

const publicPollSelect = {
  id: true,
  slug: true,
  title: true,
  description: true,
  type: true,
  status: true,
  identityMode: true,
  resultVisibility: true,
  allowComments: true,
  isPrivate: true,
  closesAt: true,
  createdByUserId: true,
  createdAt: true,
  options: {
    select: { id: true, label: true, sortOrder: true, _count: { select: { votes: true } } },
    orderBy: { sortOrder: 'asc' as const },
  },
  _count: { select: { votes: true, comments: true } },
  comments: {
    select: { id: true, authorName: true, content: true, createdAt: true },
    orderBy: { createdAt: 'desc' as const },
  },
  createdBy: { select: { name: true } },
};

class StrawPollsService {
  // ===== Admin CRUD =====

  async findAll(filters: ListStrawPollsInput) {
    const { page, limit, search, status, type } = filters;
    const where: Prisma.StrawPollWhereInput = {};

    if (status) where.status = status;
    if (type) where.type = type;
    if (search) {
      where.OR = [
        { title: { contains: search, mode: 'insensitive' } },
        { description: { contains: search, mode: 'insensitive' } },
      ];
    }

    const [polls, total] = await Promise.all([
      prisma.strawPoll.findMany({
        where,
        select: pollListSelect,
        orderBy: { createdAt: 'desc' },
        skip: (page - 1) * limit,
        take: limit,
      }),
      prisma.strawPoll.count({ where }),
    ]);

    return { polls, total, page, limit };
  }

  async findById(id: string) {
    return prisma.strawPoll.findUnique({ where: { id }, select: pollDetailSelect });
  }

  async create(data: CreateStrawPollInput, userId: string) {
    const slug = generateSlug(data.title);

    // For YES_NO_ABSTAIN, auto-create the three fixed options
    const options = data.type === StrawPollType.YES_NO_ABSTAIN
      ? [
          { label: 'Yes', sortOrder: 0 },
          { label: 'No', sortOrder: 1 },
          { label: 'Abstain', sortOrder: 2 },
        ]
      : (data.options ?? []).map((opt, i) => ({ label: opt.label, sortOrder: i }));

    if (data.type === StrawPollType.SINGLE_CHOICE && options.length < 2) {
      throw new Error('SINGLE_CHOICE polls require at least 2 options');
    }

    const poll = await prisma.strawPoll.create({
      data: {
        slug,
        title: data.title,
        description: data.description,
        type: data.type,
        identityMode: data.identityMode,
        resultVisibility: data.resultVisibility,
        allowComments: data.allowComments,
        closesAt: data.closesAt ? new Date(data.closesAt) : undefined,
        closeThreshold: data.closeThreshold,
        isPrivate: data.isPrivate,
        createdByUserId: userId,
        options: { create: options },
      },
      select: pollDetailSelect,
    });

    // Schedule auto-close if closesAt is set
    if (poll.closesAt) {
      try {
        const { pollAutoCloseQueueService } = await import('../../services/poll-auto-close-queue.service');
        const jobId = await pollAutoCloseQueueService.scheduleJob(poll.id, poll.closesAt);
        if (jobId) {
          await prisma.strawPoll.update({ where: { id: poll.id }, data: { autoCloseJobId: jobId } });
        }
      } catch (err) {
        logger.error('Failed to schedule auto-close job', { error: err, pollId: poll.id });
      }
    }

    return poll;
  }

  async update(id: string, data: UpdateStrawPollInput) {
    const existing = await prisma.strawPoll.findUnique({ where: { id } });
    if (!existing) return null;

    // Handle options update for SINGLE_CHOICE polls
    if (data.options && existing.type === StrawPollType.SINGLE_CHOICE) {
      // Delete removed options, update existing, create new
      const existingOptions = await prisma.strawPollOption.findMany({ where: { pollId: id } });
      const incomingIds = data.options.filter(o => o.id).map(o => o.id!);
      const toDelete = existingOptions.filter(o => !incomingIds.includes(o.id));

      await prisma.$transaction([
        // Delete removed options (and their votes)
        ...toDelete.map(o => prisma.strawPollOption.delete({ where: { id: o.id } })),
        // Upsert remaining
        ...data.options.map((opt, i) =>
          opt.id
            ? prisma.strawPollOption.update({ where: { id: opt.id }, data: { label: opt.label, sortOrder: i } })
            : prisma.strawPollOption.create({ data: { pollId: id, label: opt.label, sortOrder: i } })
        ),
      ]);
    }

    const { options: _, ...updateData } = data;
    const poll = await prisma.strawPoll.update({
      where: { id },
      data: {
        ...updateData,
        closesAt: data.closesAt === null ? null : data.closesAt ? new Date(data.closesAt) : undefined,
      },
      select: pollDetailSelect,
    });

    // Reschedule auto-close if closesAt changed
    if (data.closesAt !== undefined) {
      try {
        const { pollAutoCloseQueueService } = await import('../../services/poll-auto-close-queue.service');
        if (existing.autoCloseJobId) await pollAutoCloseQueueService.cancelJob(existing.id);
        if (poll.closesAt) {
          const jobId = await pollAutoCloseQueueService.scheduleJob(poll.id, poll.closesAt);
          if (jobId) await prisma.strawPoll.update({ where: { id }, data: { autoCloseJobId: jobId } });
        }
      } catch (err) {
        logger.error('Failed to reschedule auto-close job', { error: err, pollId: id });
      }
    }

    return poll;
  }

  async delete(id: string) {
    const existing = await prisma.strawPoll.findUnique({ where: { id } });
    if (!existing) return null;

    // Cancel auto-close job if scheduled
    if (existing.autoCloseJobId) {
      try {
        const { pollAutoCloseQueueService } = await import('../../services/poll-auto-close-queue.service');
        await pollAutoCloseQueueService.cancelJob(existing.id);
      } catch {}
    }

    return prisma.strawPoll.delete({ where: { id } });
  }

  // ===== Lifecycle transitions =====

  async activate(id: string) {
    return prisma.strawPoll.update({
      where: { id, status: StrawPollStatus.DRAFT },
      data: { status: StrawPollStatus.ACTIVE },
      select: pollDetailSelect,
    });
  }

  async closePoll(id: string) {
    const poll = await prisma.strawPoll.updateMany({
      where: { id, status: StrawPollStatus.ACTIVE },
      data: { status: StrawPollStatus.CLOSED },
    });
    if (poll.count === 0) return null;

    const closed = await prisma.strawPoll.findUnique({
      where: { id },
      select: { slug: true, title: true, votes: { select: { userId: true }, where: { userId: { not: null } } } },
    });

    // Broadcast poll closed via SSE
    if (closed) {
      pollSseService.broadcast(closed.slug, 'poll_closed', { pollId: id });
    }

    // Notify authenticated voters (fire-and-forget)
    if (closed?.votes) {
      this.notifyVotersPollClosed(id, closed.title, closed.votes.map(v => v.userId!)).catch(() => {});
    }

    return prisma.strawPoll.findUnique({ where: { id }, select: pollDetailSelect });
  }

  async reopenPoll(id: string) {
    return prisma.strawPoll.update({
      where: { id, status: StrawPollStatus.CLOSED },
      data: { status: StrawPollStatus.ACTIVE },
      select: pollDetailSelect,
    });
  }

  async archivePoll(id: string) {
    return prisma.strawPoll.update({
      where: { id, status: StrawPollStatus.CLOSED },
      data: { status: StrawPollStatus.ARCHIVED },
      select: pollDetailSelect,
    });
  }

  // ===== Public endpoints =====

  async findAllPublic(filters: ListStrawPollsInput) {
    const { page, limit, search } = filters;
    const where: Prisma.StrawPollWhereInput = {
      status: StrawPollStatus.ACTIVE,
      isPrivate: false,
    };
    if (search) {
      where.OR = [
        { title: { contains: search, mode: 'insensitive' } },
        { description: { contains: search, mode: 'insensitive' } },
      ];
    }

    const [polls, total] = await Promise.all([
      prisma.strawPoll.findMany({
        where,
        select: {
          id: true,
          slug: true,
          title: true,
          description: true,
          type: true,
          status: true,
          closesAt: true,
          createdAt: true,
          _count: { select: { votes: true, options: true } },
          createdBy: { select: { name: true } },
        },
        orderBy: { createdAt: 'desc' },
        skip: (page - 1) * limit,
        take: limit,
      }),
      prisma.strawPoll.count({ where }),
    ]);

    return { polls, total, page, limit };
  }

  async findBySlugPublic(slug: string, userId?: string, voterToken?: string, voterIp?: string) {
    const poll = await prisma.strawPoll.findUnique({
      where: { slug },
      select: publicPollSelect,
    });

    if (!poll) return null;
    if (poll.isPrivate && !userId) {
      // Return limited metadata for private polls when not authenticated
      return {
        id: poll.id,
        slug: poll.slug,
        title: poll.title,
        type: poll.type,
        status: poll.status,
        isPrivate: true,
        requiresAuth: true,
      };
    }

    // Determine if results should be shown
    const showResults = this.shouldShowResults(poll, userId, voterToken, voterIp);

    // Strip vote counts if results are hidden
    const options = poll.options.map(opt => ({
      id: opt.id,
      label: opt.label,
      sortOrder: opt.sortOrder,
      voteCount: showResults ? opt._count.votes : undefined,
    }));

    // Check if the requester has already voted
    const hasVoted = await this.checkHasVoted(poll.id, userId, voterToken, voterIp);

    return {
      ...poll,
      options,
      totalVotes: showResults ? poll._count.votes : undefined,
      showResults,
      hasVoted,
    };
  }

  async findBySlugWidget(slug: string) {
    const poll = await prisma.strawPoll.findUnique({
      where: { slug },
      select: {
        id: true,
        slug: true,
        title: true,
        type: true,
        status: true,
        resultVisibility: true,
        identityMode: true,
        options: {
          select: { id: true, label: true, sortOrder: true, _count: { select: { votes: true } } },
          orderBy: { sortOrder: 'asc' },
        },
        _count: { select: { votes: true } },
      },
    });
    if (!poll) return null;

    // Widget always returns counts for LIVE/PUBLIC_ALWAYS; otherwise omit
    const showCounts = poll.resultVisibility === 'LIVE' || poll.resultVisibility === 'PUBLIC_ALWAYS';

    return {
      id: poll.id,
      slug: poll.slug,
      title: poll.title,
      type: poll.type,
      status: poll.status,
      identityMode: poll.identityMode,
      totalVotes: showCounts ? poll._count.votes : 0,
      options: poll.options.map(o => ({
        id: o.id,
        label: o.label,
        sortOrder: o.sortOrder,
        voteCount: showCounts ? o._count.votes : 0,
      })),
    };
  }

  // ===== Voting =====

  async submitVote(
    slug: string,
    data: SubmitStrawPollVoteInput,
    userId?: string,
    clientIp?: string,
  ) {
    const poll = await prisma.strawPoll.findUnique({
      where: { slug },
      select: { id: true, status: true, identityMode: true, closeThreshold: true, slug: true, _count: { select: { votes: true } } },
    });

    if (!poll) throw new Error('Poll not found');
    if (poll.status !== StrawPollStatus.ACTIVE) throw new Error('Poll is not active');

    // Validate option exists
    const option = await prisma.strawPollOption.findFirst({
      where: { id: data.optionId, pollId: poll.id },
    });
    if (!option) throw new Error('Invalid option');

    // Enforce identity mode
    const { identityMode } = poll;
    if (identityMode === 'AUTHENTICATED' && !userId) {
      throw new Error('Authentication required to vote');
    }
    if (identityMode === 'TOKEN_GATED' && !data.voterToken) {
      throw new Error('A voting token is required');
    }

    // Determine dedup key
    let voterToken = data.voterToken || null;
    const voterIp = (identityMode === 'ANONYMOUS' && !userId) ? clientIp : null;

    // For anonymous/mixed without a token, generate one
    if (!userId && !voterToken && identityMode !== 'TOKEN_GATED') {
      voterToken = generateVoterToken();
    }

    // Upsert vote: one vote per poll per voter
    let vote;
    if (userId) {
      vote = await prisma.strawPollVote.upsert({
        where: { pollId_userId: { pollId: poll.id, userId } },
        create: {
          pollId: poll.id,
          optionId: data.optionId,
          userId,
          voterName: data.voterName,
          voterToken,
        },
        update: { optionId: data.optionId, updatedAt: new Date() },
      });
    } else if (voterToken) {
      vote = await prisma.strawPollVote.upsert({
        where: { pollId_voterToken: { pollId: poll.id, voterToken } },
        create: {
          pollId: poll.id,
          optionId: data.optionId,
          voterName: data.voterName,
          voterToken,
          voterIp,
        },
        update: { optionId: data.optionId, updatedAt: new Date() },
      });
    } else if (voterIp) {
      vote = await prisma.strawPollVote.upsert({
        where: { pollId_voterIp: { pollId: poll.id, voterIp } },
        create: {
          pollId: poll.id,
          optionId: data.optionId,
          voterName: data.voterName,
          voterIp,
        },
        update: { optionId: data.optionId, updatedAt: new Date() },
      });
    } else {
      throw new Error('Unable to identify voter');
    }

    // Get updated counts for SSE broadcast
    const optionCounts = await prisma.strawPollOption.findMany({
      where: { pollId: poll.id },
      select: { id: true, _count: { select: { votes: true } } },
      orderBy: { sortOrder: 'asc' },
    });
    const totalVotes = optionCounts.reduce((sum, o) => sum + o._count.votes, 0);

    // Broadcast vote update via SSE
    pollSseService.broadcast(poll.slug, 'vote_update', {
      optionCounts: optionCounts.map(o => ({ optionId: o.id, count: o._count.votes })),
      totalVotes,
    });

    // Check auto-close threshold
    if (poll.closeThreshold && totalVotes >= poll.closeThreshold) {
      this.closePoll(poll.id).catch(err =>
        logger.error('Auto-close by threshold failed', { error: err, pollId: poll.id })
      );
    }

    return { voteId: vote.id, voterToken: voterToken || undefined };
  }

  // ===== Comments =====

  async addComment(slug: string, data: SubmitStrawPollCommentInput, userId?: string) {
    const poll = await prisma.strawPoll.findUnique({
      where: { slug },
      select: { id: true, allowComments: true, slug: true },
    });
    if (!poll) throw new Error('Poll not found');
    if (!poll.allowComments) throw new Error('Comments are disabled');

    const comment = await prisma.strawPollComment.create({
      data: {
        pollId: poll.id,
        userId,
        authorName: data.authorName,
        content: data.content,
      },
      select: { id: true, authorName: true, content: true, createdAt: true },
    });

    pollSseService.broadcast(poll.slug, 'comment_added', comment);
    return comment;
  }

  async deleteComment(pollId: string, commentId: string) {
    return prisma.strawPollComment.delete({ where: { id: commentId, pollId } });
  }

  // ===== Vote moderation =====

  async deleteVote(pollId: string, voteId: string) {
    return prisma.strawPollVote.delete({ where: { id: voteId, pollId } });
  }

  // ===== Challenges =====

  async challengeFriend(pollId: string, challengerUserId: string, challengedUserId: string) {
    const poll = await prisma.strawPoll.findUnique({ where: { id: pollId }, select: { slug: true, title: true } });
    if (!poll) throw new Error('Poll not found');

    const challenge = await prisma.strawPollChallenge.create({
      data: { pollId, challengerUserId, challengedUserId },
    });

    // Send notification (fire-and-forget)
    this.sendChallengeNotification(challengedUserId, challengerUserId, poll.slug, poll.title).catch(() => {});

    return challenge;
  }

  // ===== TOKEN_GATED link generation =====

  async generateVotingTokens(pollId: string, count: number) {
    const poll = await prisma.strawPoll.findUnique({ where: { id: pollId }, select: { identityMode: true, slug: true } });
    if (!poll) throw new Error('Poll not found');
    if (poll.identityMode !== 'TOKEN_GATED') throw new Error('Poll is not token-gated');

    const tokens: string[] = [];
    for (let i = 0; i < count; i++) {
      tokens.push(generateVoterToken());
    }

    return { tokens, slug: poll.slug };
  }

  // ===== Private helpers =====

  private shouldShowResults(
    poll: { resultVisibility: string; status: string; createdByUserId: string; id: string },
    userId?: string,
    voterToken?: string,
    voterIp?: string,
  ): boolean {
    switch (poll.resultVisibility) {
      case 'LIVE':
      case 'PUBLIC_ALWAYS':
        return true;
      case 'AFTER_CLOSE':
        return poll.status === 'CLOSED' || poll.status === 'ARCHIVED';
      case 'CREATOR_ONLY':
        return !!userId && userId === poll.createdByUserId;
      case 'AFTER_VOTE':
        // Will be checked after hasVoted query — return true optimistically,
        // actual filtering happens in findBySlugPublic
        return true; // placeholder; refined by hasVoted check in caller
      default:
        return false;
    }
  }

  private async checkHasVoted(pollId: string, userId?: string, voterToken?: string, voterIp?: string): Promise<boolean> {
    if (userId) {
      const vote = await prisma.strawPollVote.findUnique({ where: { pollId_userId: { pollId, userId } } });
      return !!vote;
    }
    if (voterToken) {
      const vote = await prisma.strawPollVote.findUnique({ where: { pollId_voterToken: { pollId, voterToken } } });
      return !!vote;
    }
    if (voterIp) {
      const vote = await prisma.strawPollVote.findUnique({ where: { pollId_voterIp: { pollId, voterIp } } });
      return !!vote;
    }
    return false;
  }

  private async notifyVotersPollClosed(pollId: string, title: string, voterUserIds: string[]) {
    try {
      const { notificationService } = await import('../social/notification.service');
      for (const userId of voterUserIds) {
        await notificationService.createNotification(
          userId,
          'poll_closed' as any,
          'Poll Closed',
          `The poll "${title}" has closed. Check the results!`,
          { pollId },
        );
      }
    } catch (err) {
      logger.error('Failed to send poll closed notifications', { error: err, pollId });
    }
  }

  private async sendChallengeNotification(
    challengedUserId: string,
    challengerUserId: string,
    pollSlug: string,
    pollTitle: string,
  ) {
    try {
      const challenger = await prisma.user.findUnique({ where: { id: challengerUserId }, select: { name: true } });
      const { notificationService } = await import('../social/notification.service');
      await notificationService.createNotification(
        challengedUserId,
        'poll_challenge' as any,
        'Poll Challenge',
        `${challenger?.name || 'Someone'} challenged you to vote on "${pollTitle}"`,
        { pollSlug, challengerUserId },
      );
    } catch (err) {
      logger.error('Failed to send challenge notification', { error: err });
    }
  }
}

export const strawPollsService = new StrawPollsService();
