import {
  SharedViewScope,
  SharedViewMemberStatus,
  CalendarVisibility,
  CalendarShowDetailsTo,
  CalendarLayerType,
  Prisma,
} from '@prisma/client';
import { prisma } from '../../config/database';
import { friendshipService } from '../social/friendship.service';
import { notificationService } from '../social/notification.service';
import { calendarService } from './calendar.service';
import { escapeHtml } from '../../utils/escapeHtml';
import { AppError } from '../../middleware/error-handler';
import { logger } from '../../utils/logger';
import crypto from 'crypto';

import type {
  CreateSharedViewInput,
  UpdateSharedViewInput,
  CreateCommentInput,
  CreateReactionInput,
} from './shared-calendar.schemas';

const MEMBER_COLORS = [
  '#1890ff', '#52c41a', '#fa8c16', '#eb2f96', '#722ed1',
  '#13c2c2', '#faad14', '#f5222d', '#2f54eb', '#a0d911',
];

interface PersonalCalendarItem {
  id: string;
  type: string;
  title: string;
  date: string;
  startTime: string;
  endTime: string;
  location: string | null;
  color: string | null;
  itemType: string;
  layerId: string;
  layerName: string;
  layerColor: string;
}

interface SharedViewItem extends PersonalCalendarItem {
  memberUserId: string;
  memberName: string;
  memberColor: string;
}

interface AvailabilitySlot {
  time: string;
  members: { userId: string; userName: string; status: 'free' | 'busy' }[];
  allFree: boolean;
}

function pickColor(usedColors: string[]): string {
  const unused = MEMBER_COLORS.find(c => !usedColors.includes(c));
  if (unused) return unused;
  return MEMBER_COLORS[usedColors.length % MEMBER_COLORS.length];
}

export const sharedCalendarService = {
  // =========================================================================
  // Shared View CRUD
  // =========================================================================

  async listSharedViews(userId: string) {
    return prisma.sharedCalendarView.findMany({
      where: {
        OR: [
          { ownerId: userId },
          { members: { some: { userId, status: { in: ['INVITED', 'ACCEPTED'] } } } },
        ],
      },
      include: {
        owner: { select: { id: true, name: true, email: true } },
        members: {
          include: { user: { select: { id: true, name: true, email: true } } },
        },
        _count: { select: { members: true } },
      },
      orderBy: { createdAt: 'desc' },
    });
  },

  async createSharedView(userId: string, data: CreateSharedViewInput) {
    const view = await prisma.sharedCalendarView.create({
      data: {
        name: data.name,
        description: data.description,
        ownerId: userId,
        includedLayerTypes: data.includedLayerTypes as unknown as Prisma.InputJsonValue,
        shareScope: data.shareScope as SharedViewScope,
      },
    });

    // Auto-add owner as ACCEPTED member
    await prisma.sharedCalendarMember.create({
      data: {
        viewId: view.id,
        userId,
        status: SharedViewMemberStatus.ACCEPTED,
        color: MEMBER_COLORS[0],
        joinedAt: new Date(),
      },
    });

    return prisma.sharedCalendarView.findUnique({
      where: { id: view.id },
      include: {
        owner: { select: { id: true, name: true, email: true } },
        members: {
          include: { user: { select: { id: true, name: true, email: true } } },
        },
      },
    });
  },

  async updateSharedView(userId: string, viewId: string, data: UpdateSharedViewInput) {
    const view = await prisma.sharedCalendarView.findFirst({
      where: { id: viewId, ownerId: userId },
    });
    if (!view) throw new AppError(404, 'Shared view not found or not owner', 'NOT_FOUND');

    return prisma.sharedCalendarView.update({
      where: { id: viewId },
      data: {
        ...(data.name !== undefined && { name: data.name }),
        ...(data.description !== undefined && { description: data.description }),
        ...(data.includedLayerTypes !== undefined && {
          includedLayerTypes: data.includedLayerTypes as unknown as Prisma.InputJsonValue,
        }),
        ...(data.shareScope !== undefined && { shareScope: data.shareScope as SharedViewScope }),
      },
      include: {
        owner: { select: { id: true, name: true, email: true } },
        members: {
          include: { user: { select: { id: true, name: true, email: true } } },
        },
      },
    });
  },

  async deleteSharedView(userId: string, viewId: string) {
    const view = await prisma.sharedCalendarView.findFirst({
      where: { id: viewId, ownerId: userId },
    });
    if (!view) throw new AppError(404, 'Shared view not found or not owner', 'NOT_FOUND');

    await prisma.sharedCalendarView.delete({ where: { id: viewId } });
  },

  // =========================================================================
  // Member Management
  // =========================================================================

  async inviteMembers(userId: string, viewId: string, userIds: string[]) {
    const view = await prisma.sharedCalendarView.findFirst({
      where: { id: viewId, ownerId: userId },
      include: { members: true },
    });
    if (!view) throw new AppError(404, 'Shared view not found or not owner', 'NOT_FOUND');

    const usedColors = view.members.map(m => m.color);
    const results: { userId: string; status: string }[] = [];

    for (const targetId of userIds) {
      // Skip if already a member
      if (view.members.some(m => m.userId === targetId)) {
        results.push({ userId: targetId, status: 'already_member' });
        continue;
      }

      // Block check
      const blocked = await friendshipService.isBlocked(userId, targetId);
      if (blocked) {
        results.push({ userId: targetId, status: 'blocked' });
        continue;
      }

      const color = pickColor(usedColors);
      usedColors.push(color);

      await prisma.sharedCalendarMember.create({
        data: {
          viewId,
          userId: targetId,
          status: SharedViewMemberStatus.INVITED,
          color,
        },
      });

      // Fire notification
      const owner = await prisma.user.findUnique({ where: { id: userId }, select: { name: true, email: true } });
      const ownerName = owner?.name || owner?.email || 'Someone';
      notificationService.createNotification(
        targetId,
        'shared_view_invite',
        'Shared Calendar Invite',
        `${ownerName} invited you to "${view.name}"`,
        { viewId, fromUserId: userId },
      ).catch(() => {});

      results.push({ userId: targetId, status: 'invited' });
    }

    return results;
  },

  async respondToInvite(userId: string, viewId: string, status: 'ACCEPTED' | 'DECLINED') {
    const member = await prisma.sharedCalendarMember.findFirst({
      where: { viewId, userId, status: SharedViewMemberStatus.INVITED },
    });
    if (!member) throw new AppError(404, 'Invite not found', 'NOT_FOUND');

    const updated = await prisma.sharedCalendarMember.update({
      where: { id: member.id },
      data: {
        status: status as SharedViewMemberStatus,
        ...(status === 'ACCEPTED' && { joinedAt: new Date() }),
      },
    });

    if (status === 'ACCEPTED') {
      const view = await prisma.sharedCalendarView.findUnique({
        where: { id: viewId },
        select: { ownerId: true, name: true },
      });
      if (view) {
        const user = await prisma.user.findUnique({ where: { id: userId }, select: { name: true, email: true } });
        const userName = user?.name || user?.email || 'Someone';
        notificationService.createNotification(
          view.ownerId,
          'shared_view_accepted',
          'Invite Accepted',
          `${userName} joined "${view.name}"`,
          { viewId, fromUserId: userId },
        ).catch(() => {});
      }
    }

    return updated;
  },

  async leaveView(userId: string, viewId: string) {
    const view = await prisma.sharedCalendarView.findUnique({ where: { id: viewId } });
    if (!view) throw new AppError(404, 'Shared view not found', 'NOT_FOUND');
    if (view.ownerId === userId) {
      throw new AppError(400, 'Owner cannot leave view, delete it instead', 'OWNER_CANNOT_LEAVE');
    }

    const member = await prisma.sharedCalendarMember.findFirst({
      where: { viewId, userId },
    });
    if (!member) throw new AppError(404, 'Not a member of this view', 'NOT_FOUND');

    await prisma.sharedCalendarMember.delete({ where: { id: member.id } });
  },

  async getMembers(viewId: string, requestingUserId: string) {
    // Validate membership
    await this.validateMembership(viewId, requestingUserId);

    return prisma.sharedCalendarMember.findMany({
      where: { viewId },
      include: { user: { select: { id: true, name: true, email: true } } },
      orderBy: { joinedAt: 'asc' },
    });
  },

  // =========================================================================
  // Merged Calendar
  // =========================================================================

  async getSharedViewItems(
    viewId: string,
    requestingUserId: string,
    startDate: string,
    endDate: string
  ): Promise<SharedViewItem[]> {
    await this.validateMembership(viewId, requestingUserId);

    const view = await prisma.sharedCalendarView.findUnique({
      where: { id: viewId },
      include: {
        members: {
          where: { status: SharedViewMemberStatus.ACCEPTED },
          include: { user: { select: { id: true, name: true, email: true } } },
        },
      },
    });
    if (!view) throw new AppError(404, 'View not found', 'NOT_FOUND');

    const includedLayerTypes = (view.includedLayerTypes as string[]) || [];
    const allItems: SharedViewItem[] = [];

    for (const member of view.members) {
      const items = await this.getVisibleItems(
        member.userId,
        requestingUserId,
        startDate,
        endDate,
        includedLayerTypes,
      );

      const memberName = member.user.name || member.user.email;
      for (const item of items) {
        allItems.push({
          ...item,
          memberUserId: member.userId,
          memberName,
          memberColor: member.color,
        });
      }
    }

    // Sort by date then time
    allItems.sort((a, b) => {
      const dc = a.date.localeCompare(b.date);
      return dc !== 0 ? dc : a.startTime.localeCompare(b.startTime);
    });

    return allItems;
  },

  async getVisibleItems(
    memberId: string,
    viewerUserId: string,
    startDate: string,
    endDate: string,
    layerTypeFilter: string[],
  ): Promise<PersonalCalendarItem[]> {
    const isSelf = memberId === viewerUserId;
    const start = new Date(startDate);
    const end = new Date(endDate);

    // Get member's layers
    const layers = await prisma.calendarLayer.findMany({
      where: { userId: memberId, isEnabled: true },
    });

    // Filter by included layer types
    const filteredLayers = layers.filter(l => {
      if (layerTypeFilter.length === 0) return true;
      if (l.layerType === CalendarLayerType.SYSTEM && l.systemType) {
        return layerTypeFilter.includes(l.systemType);
      }
      return layerTypeFilter.includes(l.layerType);
    });

    // Check visibility
    let friendIds: string[] | null = null;
    const getFriendIds = async () => {
      if (friendIds === null) {
        friendIds = await friendshipService.getFriendIds(viewerUserId);
      }
      return friendIds;
    };

    const visibleLayers = [];
    for (const layer of filteredLayers) {
      if (isSelf) {
        visibleLayers.push(layer);
        continue;
      }
      if (layer.visibility === CalendarVisibility.PUBLIC) {
        visibleLayers.push(layer);
      } else if (layer.visibility === CalendarVisibility.FRIENDS) {
        const fids = await getFriendIds();
        if (fids.includes(memberId)) {
          visibleLayers.push(layer);
        }
      }
      // PRIVATE layers only visible to self
    }

    if (visibleLayers.length === 0) return [];

    const layerMap = new Map(visibleLayers.map(l => [l.id, l]));
    const results: PersonalCalendarItem[] = [];

    // User calendar items
    const userLayerIds = visibleLayers
      .filter(l => l.layerType !== CalendarLayerType.SYSTEM)
      .map(l => l.id);

    if (userLayerIds.length > 0) {
      const items = await prisma.calendarItem.findMany({
        where: {
          userId: memberId,
          layerId: { in: userLayerIds },
          date: { gte: start, lte: end },
        },
        orderBy: [{ date: 'asc' }, { startTime: 'asc' }],
        include: {
          layer: { select: { name: true, color: true, layerType: true, systemType: true } },
        },
      });

      for (const item of items) {
        // Per-item visibility override
        if (!isSelf && item.visibility) {
          if (item.visibility === CalendarVisibility.PRIVATE) continue;
          if (item.visibility === CalendarVisibility.FRIENDS) {
            const fids = await getFriendIds();
            if (!fids.includes(memberId)) continue;
          }
        }

        const redacted = shouldRedactDetails(item.showDetailsTo, isSelf, memberId, await getFriendIds());
        const layer = layerMap.get(item.layerId)!;

        results.push({
          id: item.id,
          type: 'calendar_item',
          title: redacted ? 'Busy' : item.title,
          date: item.date.toISOString().split('T')[0],
          startTime: item.startTime,
          endTime: item.endTime,
          location: redacted ? null : item.location,
          color: item.color ?? layer.color,
          itemType: item.itemType,
          layerId: layer.id,
          layerName: layer.name,
          layerColor: layer.color,
        });
      }
    }

    // System layers: shifts, tickets, polls
    const systemLayers = visibleLayers.filter(l => l.layerType === CalendarLayerType.SYSTEM);
    for (const sysLayer of systemLayers) {
      const sysItems = await this.getSystemLayerItems(memberId, sysLayer, start, end);
      results.push(...sysItems);
    }

    return results;
  },

  async getSystemLayerItems(
    userId: string,
    layer: { id: string; name: string; color: string; systemType: string | null },
    start: Date,
    end: Date,
  ): Promise<PersonalCalendarItem[]> {
    const items: PersonalCalendarItem[] = [];

    if (layer.systemType === 'SHIFTS') {
      try {
        const signups = await prisma.shiftSignup.findMany({
          where: {
            userId,
            status: 'CONFIRMED',
            shift: { status: { not: 'CANCELLED' }, date: { gte: start, lte: end } },
          },
          include: {
            shift: { select: { id: true, title: true, date: true, startTime: true, endTime: true, location: true } },
          },
        });
        for (const signup of signups) {
          const s = signup.shift;
          items.push({
            id: `shift-${s.id}`,
            type: 'shift',
            title: s.title,
            date: s.date.toISOString().split('T')[0],
            startTime: s.startTime,
            endTime: s.endTime,
            location: s.location,
            color: layer.color,
            itemType: 'EVENT',
            layerId: layer.id,
            layerName: layer.name,
            layerColor: layer.color,
          });
        }
      } catch (err) {
        logger.debug('Failed to fetch shifts for shared view:', err);
      }
    }

    if (layer.systemType === 'TICKETS') {
      try {
        const tickets = await prisma.ticket.findMany({
          where: {
            userId,
            status: 'VALID',
            event: { date: { gte: start, lte: end } },
          },
          include: {
            event: { select: { id: true, title: true, date: true, startTime: true, endTime: true, venueName: true } },
          },
        });
        for (const ticket of tickets) {
          const e = ticket.event;
          items.push({
            id: `ticket-${ticket.id}`,
            type: 'ticket',
            title: e.title,
            date: e.date.toISOString().split('T')[0],
            startTime: e.startTime,
            endTime: e.endTime,
            location: e.venueName,
            color: layer.color,
            itemType: 'EVENT',
            layerId: layer.id,
            layerName: layer.name,
            layerColor: layer.color,
          });
        }
      } catch (err) {
        logger.debug('Failed to fetch tickets for shared view:', err);
      }
    }

    if (layer.systemType === 'POLLS') {
      try {
        const votes = await prisma.schedulingPollVote.findMany({
          where: {
            userId,
            option: { date: { gte: start, lte: end } },
          },
          include: {
            poll: { select: { id: true, title: true, location: true } },
            option: { select: { id: true, date: true, startTime: true, endTime: true } },
          },
        });
        const seen = new Set<string>();
        for (const vote of votes) {
          const key = `${vote.poll.id}-${vote.option.id}`;
          if (seen.has(key)) continue;
          seen.add(key);
          items.push({
            id: `poll-${vote.poll.id}-${vote.option.id}`,
            type: 'poll',
            title: vote.poll.title,
            date: vote.option.date.toISOString().split('T')[0],
            startTime: vote.option.startTime,
            endTime: vote.option.endTime,
            location: vote.poll.location,
            color: layer.color,
            itemType: 'EVENT',
            layerId: layer.id,
            layerName: layer.name,
            layerColor: layer.color,
          });
        }
      } catch (err) {
        logger.debug('Failed to fetch polls for shared view:', err);
      }
    }

    return items;
  },

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

  async getComments(viewId: string, userId: string, date: string) {
    await this.validateMembership(viewId, userId);

    return prisma.sharedViewComment.findMany({
      where: { viewId, itemDate: date },
      include: { user: { select: { id: true, name: true, email: true } } },
      orderBy: { createdAt: 'asc' },
    });
  },

  async createComment(viewId: string, userId: string, data: CreateCommentInput) {
    await this.validateAcceptedMember(viewId, userId);

    const comment = await prisma.sharedViewComment.create({
      data: {
        viewId,
        userId,
        itemDate: data.date,
        itemId: data.itemId,
        content: escapeHtml(data.content),
      },
      include: { user: { select: { id: true, name: true, email: true } } },
    });

    // Notify other accepted members (fire-and-forget)
    this.notifyMembers(viewId, userId, 'comment', comment.user.name || comment.user.email).catch(() => {});

    return comment;
  },

  async deleteComment(commentId: string, userId: string) {
    const comment = await prisma.sharedViewComment.findUnique({
      where: { id: commentId },
      include: { view: { select: { ownerId: true } } },
    });
    if (!comment) throw new AppError(404, 'Comment not found', 'NOT_FOUND');

    if (comment.userId !== userId && comment.view.ownerId !== userId) {
      throw new AppError(403, 'Not authorized to delete this comment', 'FORBIDDEN');
    }

    await prisma.sharedViewComment.delete({ where: { id: commentId } });
  },

  // =========================================================================
  // Reactions
  // =========================================================================

  async addReaction(viewId: string, userId: string, data: CreateReactionInput) {
    await this.validateAcceptedMember(viewId, userId);

    return prisma.sharedViewReaction.upsert({
      where: {
        viewId_userId_itemId_emoji: {
          viewId,
          userId,
          itemId: data.itemId,
          emoji: data.emoji,
        },
      },
      update: {},
      create: {
        viewId,
        userId,
        itemId: data.itemId,
        emoji: data.emoji,
      },
    });
  },

  async removeReaction(reactionId: string, userId: string) {
    const reaction = await prisma.sharedViewReaction.findUnique({
      where: { id: reactionId },
    });
    if (!reaction) throw new AppError(404, 'Reaction not found', 'NOT_FOUND');
    if (reaction.userId !== userId) {
      throw new AppError(403, 'Not authorized', 'FORBIDDEN');
    }

    await prisma.sharedViewReaction.delete({ where: { id: reactionId } });
  },

  async getReactionsForItems(viewId: string, itemIds: string[]) {
    const reactions = await prisma.sharedViewReaction.findMany({
      where: { viewId, itemId: { in: itemIds } },
      include: { user: { select: { id: true, name: true } } },
    });

    // Group by itemId
    const grouped: Record<string, {
      emoji: string;
      count: number;
      users: { id: string; name: string | null }[];
      reactionIds: string[];
    }[]> = {};

    for (const r of reactions) {
      if (!grouped[r.itemId]) grouped[r.itemId] = [];
      let emojiGroup = grouped[r.itemId].find(g => g.emoji === r.emoji);
      if (!emojiGroup) {
        emojiGroup = { emoji: r.emoji, count: 0, users: [], reactionIds: [] };
        grouped[r.itemId].push(emojiGroup);
      }
      emojiGroup.count++;
      emojiGroup.users.push({ id: r.user.id, name: r.user.name });
      emojiGroup.reactionIds.push(r.id);
    }

    return grouped;
  },

  // =========================================================================
  // Availability Finder
  // =========================================================================

  async findAvailability(
    viewId: string,
    userId: string,
    start: string,
    end: string,
    dayStart: string,
    dayEnd: string,
    slotDuration: number,
  ) {
    await this.validateMembership(viewId, userId);

    const view = await prisma.sharedCalendarView.findUnique({
      where: { id: viewId },
      include: {
        members: {
          where: { status: SharedViewMemberStatus.ACCEPTED },
          include: { user: { select: { id: true, name: true, email: true } } },
        },
      },
    });
    if (!view) throw new AppError(404, 'View not found', 'NOT_FOUND');

    const includedLayerTypes = (view.includedLayerTypes as string[]) || [];

    // Get all members' items
    const memberItems = new Map<string, PersonalCalendarItem[]>();
    for (const member of view.members) {
      const items = await this.getVisibleItems(
        member.userId,
        userId,
        start,
        end,
        includedLayerTypes,
      );
      memberItems.set(member.userId, items);
    }

    // Build time grid
    const startDate = new Date(start);
    const endDate = new Date(end);
    const dates: Record<string, { slots: AvailabilitySlot[] }> = {};

    const current = new Date(startDate);
    while (current <= endDate) {
      const dateStr = current.toISOString().split('T')[0];
      const slots: AvailabilitySlot[] = [];

      const [startH, startM] = dayStart.split(':').map(Number);
      const [endH, endM] = dayEnd.split(':').map(Number);
      const dayStartMins = startH * 60 + startM;
      const dayEndMins = endH * 60 + endM;

      for (let mins = dayStartMins; mins + slotDuration <= dayEndMins; mins += slotDuration) {
        const slotStartH = Math.floor(mins / 60);
        const slotStartM = mins % 60;
        const slotEndMins = mins + slotDuration;
        const slotEndH = Math.floor(slotEndMins / 60);
        const slotEndM = slotEndMins % 60;

        const slotStart = `${String(slotStartH).padStart(2, '0')}:${String(slotStartM).padStart(2, '0')}`;
        const slotEnd = `${String(slotEndH).padStart(2, '0')}:${String(slotEndM).padStart(2, '0')}`;

        const members: AvailabilitySlot['members'] = [];

        for (const member of view.members) {
          const items = memberItems.get(member.userId) || [];
          const memberName = member.user.name || member.user.email;

          // Check overlap: item on this date where item.startTime < slotEnd AND item.endTime > slotStart
          const isBusy = items.some(item =>
            item.date === dateStr &&
            item.startTime < slotEnd &&
            item.endTime > slotStart
          );

          members.push({
            userId: member.userId,
            userName: memberName,
            status: isBusy ? 'busy' : 'free',
          });
        }

        slots.push({
          time: slotStart,
          members,
          allFree: members.every(m => m.status === 'free'),
        });
      }

      dates[dateStr] = { slots };
      current.setDate(current.getDate() + 1);
    }

    return { dates };
  },

  // =========================================================================
  // Event Sharing
  // =========================================================================

  async shareItemWithFriends(
    userId: string,
    itemId: string,
    friendIds: string[],
    message?: string,
  ) {
    // Verify item ownership
    const item = await prisma.calendarItem.findFirst({
      where: { id: itemId, userId },
    });
    if (!item) throw new AppError(404, 'Calendar item not found', 'NOT_FOUND');

    const myFriendIds = await friendshipService.getFriendIds(userId);
    const user = await prisma.user.findUnique({ where: { id: userId }, select: { name: true, email: true } });
    const userName = user?.name || user?.email || 'Someone';

    const results: { userId: string; status: string }[] = [];

    for (const friendId of friendIds) {
      if (!myFriendIds.includes(friendId)) {
        results.push({ userId: friendId, status: 'not_friend' });
        continue;
      }

      const msg = message
        ? `${userName} shared "${item.title}" with you: ${escapeHtml(message)}`
        : `${userName} shared "${item.title}" with you`;

      notificationService.createNotification(
        friendId,
        'calendar_event_invite',
        'Calendar Event Shared',
        msg,
        { itemId, fromUserId: userId, date: item.date.toISOString().split('T')[0] },
      ).catch(() => {});

      results.push({ userId: friendId, status: 'shared' });
    }

    return results;
  },

  // =========================================================================
  // Friend Calendar
  // =========================================================================

  async getFriendPublicCalendar(
    viewerUserId: string,
    targetUserId: string,
    startDate: string,
    endDate: string,
  ) {
    // Verify accepted friendship
    const friendIds = await friendshipService.getFriendIds(viewerUserId);
    if (!friendIds.includes(targetUserId)) {
      throw new AppError(403, 'Not friends with this user', 'NOT_FRIENDS');
    }

    // Get FRIENDS and PUBLIC layers
    const layers = await prisma.calendarLayer.findMany({
      where: {
        userId: targetUserId,
        isEnabled: true,
        visibility: { in: [CalendarVisibility.FRIENDS, CalendarVisibility.PUBLIC] },
      },
    });

    const start = new Date(startDate);
    const end = new Date(endDate);
    const results: PersonalCalendarItem[] = [];

    // User calendar items from visible layers
    const userLayerIds = layers
      .filter(l => l.layerType !== CalendarLayerType.SYSTEM)
      .map(l => l.id);

    if (userLayerIds.length > 0) {
      const items = await prisma.calendarItem.findMany({
        where: {
          userId: targetUserId,
          layerId: { in: userLayerIds },
          date: { gte: start, lte: end },
        },
        orderBy: [{ date: 'asc' }, { startTime: 'asc' }],
        include: {
          layer: { select: { id: true, name: true, color: true, layerType: true, systemType: true } },
        },
      });

      for (const item of items) {
        // Per-item visibility
        if (item.visibility === CalendarVisibility.PRIVATE) continue;

        const redacted = shouldRedactDetails(item.showDetailsTo, false, targetUserId, friendIds);
        results.push({
          id: item.id,
          type: 'calendar_item',
          title: redacted ? 'Busy' : item.title,
          date: item.date.toISOString().split('T')[0],
          startTime: item.startTime,
          endTime: item.endTime,
          location: redacted ? null : item.location,
          color: item.color ?? item.layer.color,
          itemType: item.itemType,
          layerId: item.layer.id,
          layerName: item.layer.name,
          layerColor: item.layer.color,
        });
      }
    }

    // System layers
    const systemLayers = layers.filter(l => l.layerType === CalendarLayerType.SYSTEM);
    for (const sysLayer of systemLayers) {
      const sysItems = await this.getSystemLayerItems(targetUserId, sysLayer, start, end);
      results.push(...sysItems);
    }

    results.sort((a, b) => {
      const dc = a.date.localeCompare(b.date);
      return dc !== 0 ? dc : a.startTime.localeCompare(b.startTime);
    });

    return results;
  },

  // =========================================================================
  // Public Share
  // =========================================================================

  async generateShareToken(userId: string, viewId: string) {
    const view = await prisma.sharedCalendarView.findFirst({
      where: { id: viewId, ownerId: userId },
    });
    if (!view) throw new AppError(404, 'Shared view not found or not owner', 'NOT_FOUND');

    const token = crypto.randomBytes(24).toString('hex');

    await prisma.sharedCalendarView.update({
      where: { id: viewId },
      data: { shareToken: token },
    });

    return { token };
  },

  async getPublicView(shareToken: string, startDate: string, endDate: string) {
    const view = await prisma.sharedCalendarView.findFirst({
      where: { shareToken },
      include: {
        members: {
          where: { status: SharedViewMemberStatus.ACCEPTED },
          include: { user: { select: { id: true, name: true } } },
        },
      },
    });
    if (!view) throw new AppError(404, 'Shared view not found', 'NOT_FOUND');

    const includedLayerTypes = (view.includedLayerTypes as string[]) || [];
    const start = new Date(startDate);
    const end = new Date(endDate);
    const allItems: SharedViewItem[] = [];

    for (const member of view.members) {
      // For public view, only show items where showDetailsTo is EVERYONE
      const layers = await prisma.calendarLayer.findMany({
        where: {
          userId: member.userId,
          isEnabled: true,
          visibility: CalendarVisibility.PUBLIC,
        },
      });

      const filteredLayers = layers.filter(l => {
        if (includedLayerTypes.length === 0) return true;
        if (l.layerType === CalendarLayerType.SYSTEM && l.systemType) {
          return includedLayerTypes.includes(l.systemType);
        }
        return includedLayerTypes.includes(l.layerType);
      });

      const userLayerIds = filteredLayers
        .filter(l => l.layerType !== CalendarLayerType.SYSTEM)
        .map(l => l.id);

      if (userLayerIds.length > 0) {
        const items = await prisma.calendarItem.findMany({
          where: {
            userId: member.userId,
            layerId: { in: userLayerIds },
            date: { gte: start, lte: end },
            showDetailsTo: CalendarShowDetailsTo.EVERYONE,
          },
          include: {
            layer: { select: { id: true, name: true, color: true, layerType: true, systemType: true } },
          },
          orderBy: [{ date: 'asc' }, { startTime: 'asc' }],
        });

        const memberName = member.user.name || 'Member';
        for (const item of items) {
          if (item.visibility === CalendarVisibility.PRIVATE || item.visibility === CalendarVisibility.FRIENDS) continue;
          allItems.push({
            id: item.id,
            type: 'calendar_item',
            title: item.title,
            date: item.date.toISOString().split('T')[0],
            startTime: item.startTime,
            endTime: item.endTime,
            location: item.location,
            color: item.color ?? item.layer.color,
            itemType: item.itemType,
            layerId: item.layer.id,
            layerName: item.layer.name,
            layerColor: item.layer.color,
            memberUserId: member.userId,
            memberName,
            memberColor: member.color,
          });
        }
      }

      // System layers (public only)
      const sysLayers = filteredLayers.filter(l => l.layerType === CalendarLayerType.SYSTEM);
      for (const sysLayer of sysLayers) {
        const sysItems = await this.getSystemLayerItems(member.userId, sysLayer, start, end);
        const memberName = member.user.name || 'Member';
        for (const si of sysItems) {
          allItems.push({
            ...si,
            memberUserId: member.userId,
            memberName,
            memberColor: member.color,
          });
        }
      }
    }

    allItems.sort((a, b) => {
      const dc = a.date.localeCompare(b.date);
      return dc !== 0 ? dc : a.startTime.localeCompare(b.startTime);
    });

    return { name: view.name, description: view.description, items: allItems };
  },

  // =========================================================================
  // Helpers
  // =========================================================================

  async validateMembership(viewId: string, userId: string) {
    const member = await prisma.sharedCalendarMember.findFirst({
      where: { viewId, userId, status: { in: ['INVITED', 'ACCEPTED'] } },
    });
    if (!member) {
      // Check if owner
      const view = await prisma.sharedCalendarView.findFirst({
        where: { id: viewId, ownerId: userId },
      });
      if (!view) throw new AppError(403, 'Not a member of this shared view', 'NOT_MEMBER');
    }
  },

  async validateAcceptedMember(viewId: string, userId: string) {
    const member = await prisma.sharedCalendarMember.findFirst({
      where: { viewId, userId, status: SharedViewMemberStatus.ACCEPTED },
    });
    if (!member) {
      throw new AppError(403, 'Must be an accepted member', 'NOT_ACCEPTED_MEMBER');
    }
  },

  async notifyMembers(viewId: string, excludeUserId: string, type: string, authorName: string) {
    const members = await prisma.sharedCalendarMember.findMany({
      where: { viewId, status: SharedViewMemberStatus.ACCEPTED, userId: { not: excludeUserId } },
      select: { userId: true },
    });

    const view = await prisma.sharedCalendarView.findUnique({
      where: { id: viewId },
      select: { name: true },
    });

    for (const m of members) {
      notificationService.createNotification(
        m.userId,
        'comment',
        `New comment in "${view?.name || 'shared calendar'}"`,
        `${authorName} left a comment`,
        { viewId },
      ).catch(() => {});
    }
  },
};

function shouldRedactDetails(
  showDetailsTo: CalendarShowDetailsTo,
  isSelf: boolean,
  memberId: string,
  viewerFriendIds: string[],
): boolean {
  if (isSelf) return false;
  if (showDetailsTo === CalendarShowDetailsTo.EVERYONE) return false;
  if (showDetailsTo === CalendarShowDetailsTo.FRIENDS) {
    return !viewerFriendIds.includes(memberId);
  }
  // NOBODY
  return true;
}
