import { Injectable } from '@nestjs/common';
import { PrismaService } from '@core/database/prisma/prisma.service';
import {
  Prisma,
  OutlookBootstrapStatus,
  OutlookBindingSyncMode,
  OutlookCancellationSource,
  OutlookManageStatus,
  OutlookMailboxType,
  OutlookSubscriptionStatus,
} from '@prisma/client';

@Injectable()
export class OutlookSyncRepository {
  constructor(private readonly prisma: PrismaService) {}

  findMailboxById(id: string) {
    return this.prisma.outlookSyncMailbox.findUnique({
      where: { id },
      include: {
        cursor: true,
        subscriptions: {
          orderBy: { createdAt: 'desc' },
          take: 1,
        },
      },
    });
  }

  listEnabledMailboxes() {
    return this.prisma.outlookSyncMailbox.findMany({
      where: { isEnabled: true },
      orderBy: [{ isPrimaryDefault: 'desc' }, { mailboxEmail: 'asc' }],
    });
  }

  findMailboxByEmail(mailboxEmail: string) {
    return this.prisma.outlookSyncMailbox.findUnique({
      where: { mailboxEmail },
    });
  }

  createMailbox(data: {
    mailboxEmail: string;
    mailboxType: OutlookMailboxType;
    isPrimaryDefault: boolean;
    isEnabled: boolean;
  }) {
    return this.prisma.outlookSyncMailbox.create({ data });
  }

  async updateMailboxStatus(id: string, isEnabled: boolean) {
    return this.prisma.outlookSyncMailbox.update({
      where: { id },
      data: { isEnabled },
    });
  }

  async setPrimaryDefault(id: string, isPrimaryDefault: boolean) {
    return this.prisma.$transaction(async (tx) => {
      if (isPrimaryDefault) {
        await tx.outlookSyncMailbox.updateMany({
          where: { isPrimaryDefault: true },
          data: { isPrimaryDefault: false },
        });
      }

      return tx.outlookSyncMailbox.update({
        where: { id },
        data: { isPrimaryDefault },
      });
    });
  }

  getLatestActiveSubscription(mailboxId: string) {
    return this.prisma.outlookSubscription.findFirst({
      where: {
        mailboxId,
        status: {
          in: [OutlookSubscriptionStatus.ACTIVE, OutlookSubscriptionStatus.ERROR],
        },
      },
      orderBy: { createdAt: 'desc' },
    });
  }

  createSubscription(data: Prisma.OutlookSubscriptionCreateInput) {
    return this.prisma.outlookSubscription.create({ data });
  }

  updateSubscription(id: string, data: Prisma.OutlookSubscriptionUpdateInput) {
    return this.prisma.outlookSubscription.update({ where: { id }, data });
  }

  findSubscriptionByGraphSubscriptionId(graphSubscriptionId: string) {
    return this.prisma.outlookSubscription.findUnique({
      where: { graphSubscriptionId },
      include: { mailbox: true },
    });
  }

  listSubscriptionsToRenew(renewBefore: Date) {
    return this.prisma.outlookSubscription.findMany({
      where: {
        status: OutlookSubscriptionStatus.ACTIVE,
        expirationAt: { lte: renewBefore },
      },
      include: {
        mailbox: true,
      },
      orderBy: { expirationAt: 'asc' },
    });
  }

  async markMailboxSubscriptionsDisabled(mailboxId: string) {
    await this.prisma.outlookSubscription.updateMany({
      where: { mailboxId, status: { in: [OutlookSubscriptionStatus.ACTIVE, OutlookSubscriptionStatus.ERROR] } },
      data: { status: OutlookSubscriptionStatus.DISABLED },
    });
  }

  async getOrCreateSettings() {
    return this.prisma.outlookSyncSetting.upsert({
      where: { id: 'default' },
      create: { id: 'default' },
      update: {},
    });
  }

  updateSettings(data: Prisma.OutlookSyncSettingUpdateInput) {
    return this.prisma.outlookSyncSetting.update({
      where: { id: 'default' },
      data,
    });
  }

  listBindingsByMailbox(mailboxId: string) {
    return this.prisma.outlookMeetingBinding.findMany({
      where: { primaryMailboxId: mailboxId },
    });
  }

  upsertBindingByGraphEvent(
    graphEventId: string,
    data: Prisma.OutlookMeetingBindingUncheckedCreateInput,
    update: Prisma.OutlookMeetingBindingUncheckedUpdateInput,
  ) {
    return this.prisma.outlookMeetingBinding.upsert({
      where: {
        graphEventId,
      },
      create: data,
      update,
    });
  }

  findBindingByGraphEventId(graphEventId: string) {
    return this.prisma.outlookMeetingBinding.findUnique({
      where: { graphEventId },
      include: {
        primaryMailbox: true,
        meeting: true,
      },
    });
  }

  findBindingById(id: string) {
    return this.prisma.outlookMeetingBinding.findUnique({
      where: { id },
      include: {
        primaryMailbox: true,
        meeting: true,
        meetingSeries: true,
        occurrenceExclusions: {
          orderBy: { createdAt: 'desc' },
          take: 200,
        },
      },
    });
  }

  findBindingByMeetingId(meetingId: string) {
    return this.prisma.outlookMeetingBinding.findFirst({
      where: {
        meetingId,
        manageStatus: {
          in: [OutlookManageStatus.MANAGED, OutlookManageStatus.SYNC_ERROR],
        },
      },
      include: {
        primaryMailbox: true,
        meeting: true,
        meetingSeries: true,
      },
    });
  }

  findBindingsByGraphEventId(graphEventId: string) {
    return this.prisma.outlookMeetingBinding.findMany({
      where: { graphEventId },
      include: {
        meeting: true,
      },
    });
  }

  findBindingsByGraphEventIds(graphEventIds: string[]) {
    if (!graphEventIds.length) {
      return Promise.resolve([]);
    }
    return this.prisma.outlookMeetingBinding.findMany({
      where: { graphEventId: { in: graphEventIds } },
      include: {
        meeting: true,
      },
    });
  }

  listManagedSeriesBindingsByMailbox(mailboxId: string) {
    return this.prisma.outlookMeetingBinding.findMany({
      where: {
        primaryMailboxId: mailboxId,
        manageStatus: {
          in: [OutlookManageStatus.MANAGED, OutlookManageStatus.SYNC_ERROR],
        },
        graphEventType: 'seriesMaster',
      },
      include: {
        occurrenceExclusions: true,
      },
    });
  }

  listManagedSeriesBindings() {
    return this.prisma.outlookMeetingBinding.findMany({
      where: {
        manageStatus: {
          in: [OutlookManageStatus.MANAGED, OutlookManageStatus.SYNC_ERROR],
        },
        graphEventType: 'seriesMaster',
      },
      include: {
        occurrenceExclusions: true,
      },
    });
  }

  async isSeriesOccurrenceExcluded(bindingId: string, occurrenceGraphEventId: string) {
    const existed = await this.prisma.outlookSeriesOccurrenceExclusion.findUnique({
      where: {
        bindingId_occurrenceGraphEventId: {
          bindingId,
          occurrenceGraphEventId,
        },
      },
      select: { id: true },
    });
    return Boolean(existed);
  }

  createSeriesOccurrenceExclusion(data: Prisma.OutlookSeriesOccurrenceExclusionUncheckedCreateInput) {
    return this.prisma.outlookSeriesOccurrenceExclusion.upsert({
      where: {
        bindingId_occurrenceGraphEventId: {
          bindingId: data.bindingId,
          occurrenceGraphEventId: data.occurrenceGraphEventId,
        },
      },
      create: data,
      update: {
        iCalUId: data.iCalUId,
        reason: data.reason,
        createdByEmail: data.createdByEmail,
      },
    });
  }

  deleteSeriesOccurrenceExclusion(id: string) {
    return this.prisma.outlookSeriesOccurrenceExclusion.delete({
      where: { id },
    });
  }

  findSeriesOccurrenceExclusionById(id: string) {
    return this.prisma.outlookSeriesOccurrenceExclusion.findUnique({
      where: { id },
      include: {
        binding: true,
      },
    });
  }

  listSeriesOccurrenceExclusions(params: {
    bindingId: string;
    page: number;
    pageSize: number;
  }) {
    return this.prisma.outlookSeriesOccurrenceExclusion.findMany({
      where: { bindingId: params.bindingId },
      orderBy: { createdAt: 'desc' },
      skip: (params.page - 1) * params.pageSize,
      take: params.pageSize,
    });
  }

  countSeriesOccurrenceExclusions(bindingId: string) {
    return this.prisma.outlookSeriesOccurrenceExclusion.count({
      where: { bindingId },
    });
  }

  updateBindingSyncState(id: string, data: {
    manageStatus?: OutlookManageStatus;
    cancellationSource?: OutlookCancellationSource | null;
    syncMode?: OutlookBindingSyncMode;
    localOverrideAt?: Date | null;
    localOverrideByUserId?: string | null;
    localOverrideByEmail?: string | null;
    localOverrideReason?: string | null;
    localOverrideFields?: Prisma.InputJsonValue | null;
    lastSyncedAt?: Date;
    bootstrapStatus?: OutlookBootstrapStatus | null;
    bootstrapError?: string | null;
    bootstrapUpdatedAt?: Date | null;
  }) {
    const normalizedData: Prisma.OutlookMeetingBindingUpdateInput = {};
    if (data.manageStatus !== undefined) normalizedData.manageStatus = data.manageStatus;
    if (data.cancellationSource !== undefined) normalizedData.cancellationSource = data.cancellationSource;
    if (data.syncMode !== undefined) normalizedData.syncMode = data.syncMode;
    if (data.localOverrideAt !== undefined) normalizedData.localOverrideAt = data.localOverrideAt;
    if (data.localOverrideByUserId !== undefined) normalizedData.localOverrideByUserId = data.localOverrideByUserId;
    if (data.localOverrideByEmail !== undefined) normalizedData.localOverrideByEmail = data.localOverrideByEmail;
    if (data.localOverrideReason !== undefined) normalizedData.localOverrideReason = data.localOverrideReason;
    if (data.localOverrideFields !== undefined) {
      normalizedData.localOverrideFields = data.localOverrideFields === null
        ? Prisma.JsonNull
        : data.localOverrideFields;
    }
    if (data.lastSyncedAt !== undefined) normalizedData.lastSyncedAt = data.lastSyncedAt;
    if (data.bootstrapStatus !== undefined) normalizedData.bootstrapStatus = data.bootstrapStatus;
    if (data.bootstrapError !== undefined) normalizedData.bootstrapError = data.bootstrapError;
    if (data.bootstrapUpdatedAt !== undefined) normalizedData.bootstrapUpdatedAt = data.bootstrapUpdatedAt;
    return this.prisma.outlookMeetingBinding.update({
      where: { id },
      data: normalizedData,
    });
  }

  lockBindingByLocalOverride(params: {
    id: string;
    userId?: string | null;
    email?: string | null;
    reason: string;
    fields?: Prisma.InputJsonValue | null;
  }) {
    return this.prisma.outlookMeetingBinding.update({
      where: { id: params.id },
      data: {
        syncMode: OutlookBindingSyncMode.LOCKED_BY_LOCAL_EDIT,
        localOverrideAt: new Date(),
        localOverrideByUserId: params.userId || null,
        localOverrideByEmail: params.email || null,
        localOverrideReason: params.reason,
        localOverrideFields: params.fields || Prisma.JsonNull,
      },
    });
  }

  updateBindingMeetingLink(id: string, data: {
    meetingId?: string | null;
    meetingSeriesId?: string | null;
  }) {
    return this.prisma.outlookMeetingBinding.update({
      where: { id },
      data,
    });
  }

  takeoverBinding(params: {
    id: string;
    ownerUserId: string;
    ownerEmail: string;
    primaryMailboxId: string;
    syncFrom: Date;
  }) {
    return this.prisma.outlookMeetingBinding.update({
      where: { id: params.id },
      data: {
        ownerUserId: params.ownerUserId,
        ownerEmail: params.ownerEmail,
        primaryMailboxId: params.primaryMailboxId,
        syncFrom: params.syncFrom,
      },
    });
  }

  createBindingSyncEventLog(params: {
    bindingId: string;
    mailboxId: string;
    eventType: string;
    resultStatus?: string;
    message?: string | null;
    payload?: Prisma.InputJsonValue;
  }) {
    return (this.prisma as any).outlookSyncEventLog.create({
      data: {
        bindingId: params.bindingId,
        mailboxId: params.mailboxId,
        eventType: params.eventType,
        resultStatus: params.resultStatus || 'SUCCESS',
        message: params.message || null,
        payload: params.payload,
      },
    });
  }

  getLatestSourceVersion(bindingId: string) {
    return (this.prisma as any).outlookEventSourceVersion.findFirst({
      where: { bindingId },
      orderBy: [{ createdAt: 'desc' }],
    });
  }

  createSourceVersion(data: Prisma.OutlookEventSourceVersionUncheckedCreateInput) {
    return (this.prisma as any).outlookEventSourceVersion.create({ data });
  }

  createSyncDiff(data: Prisma.OutlookEventSyncDiffUncheckedCreateInput) {
    return (this.prisma as any).outlookEventSyncDiff.create({ data });
  }

  listBindingSyncEventLogs(params: {
    bindingId: string;
    page: number;
    pageSize: number;
    startDate?: Date;
    endDate?: Date;
    eventType?: string;
    stage?: string;
    onlyError?: boolean;
  }) {
    const where = this.buildBindingSyncEventLogWhere(params);
    return (this.prisma as any).outlookSyncEventLog.findMany({
      where,
      orderBy: { createdAt: 'desc' },
      skip: (params.page - 1) * params.pageSize,
      take: params.pageSize,
    });
  }

  countBindingSyncEventLogs(params: {
    bindingId: string;
    startDate?: Date;
    endDate?: Date;
    eventType?: string;
    stage?: string;
    onlyError?: boolean;
  }) {
    const where = this.buildBindingSyncEventLogWhere(params);
    return (this.prisma as any).outlookSyncEventLog.count({ where });
  }

  listBindingSyncEventLogsForExport(params: {
    bindingId: string;
    startDate?: Date;
    endDate?: Date;
    eventType?: string;
    stage?: string;
    onlyError?: boolean;
    maxRows: number;
  }) {
    const where = this.buildBindingSyncEventLogWhere(params);
    return (this.prisma as any).outlookSyncEventLog.findMany({
      where,
      orderBy: { createdAt: 'desc' },
      take: params.maxRows,
    });
  }

  private buildBindingSyncEventLogWhere(params: {
    bindingId: string;
    startDate?: Date;
    endDate?: Date;
    eventType?: string;
    stage?: string;
    onlyError?: boolean;
  }): Prisma.OutlookSyncEventLogWhereInput {
    const where: Prisma.OutlookSyncEventLogWhereInput = {
      bindingId: params.bindingId,
    };

    if (params.onlyError) {
      where.resultStatus = 'ERROR';
    }
    if (params.eventType) {
      where.eventType = params.eventType;
    }
    if (params.startDate || params.endDate) {
      where.createdAt = {};
      if (params.startDate) {
        where.createdAt.gte = params.startDate;
      }
      if (params.endDate) {
        where.createdAt.lte = params.endDate;
      }
    }
    if (params.stage) {
      if (params.stage === 'NONE') {
        where.OR = [
          { payload: { equals: Prisma.DbNull } as any },
          { payload: { equals: Prisma.JsonNull } as any },
          { payload: { path: ['stage'], equals: null } as any },
          { payload: { path: ['stage'], equals: '' } as any },
        ];
      } else {
        where.payload = { path: ['stage'], equals: params.stage } as any;
      }
    }

    return where;
  }

  listManagedBindings(params: {
    mailboxId: string;
    keyword?: string;
    eventType?: string;
    status?: string;
    page: number;
    pageSize: number;
  }) {
    const keyword = params.keyword?.trim();
    const where: Prisma.OutlookMeetingBindingWhereInput = {
      primaryMailboxId: params.mailboxId,
      manageStatus: {
        in: [OutlookManageStatus.MANAGED, OutlookManageStatus.SYNC_ERROR],
      },
      // Managed list uses grouped display (root + lazy-loaded children),
      // so pagination must be based on roots only.
      graphEventType: {
        in: ['single', 'seriesMaster'],
      },
    };
    if (keyword) {
      where.OR = [
        { graphEventId: { contains: keyword, mode: 'insensitive' } },
        { iCalUId: { contains: keyword, mode: 'insensitive' } },
        { meeting: { title: { contains: keyword, mode: 'insensitive' } } },
      ];
    }
    if (params.eventType) {
      where.graphEventType = params.eventType;
    }
    if (params.status) {
      where.meeting = {
        ...(where.meeting || {}),
        status: params.status as any,
      };
    }

    return this.prisma.outlookMeetingBinding.findMany({
      where,
      include: {
        meeting: {
          select: {
            id: true,
            title: true,
            startTime: true,
            endTime: true,
            status: true,
            timezone: true,
          },
        },
        meetingSeries: {
          select: {
            id: true,
            title: true,
            startDate: true,
            timezone: true,
          },
        },
      },
      orderBy: [{ meeting: { startTime: 'asc' } }, { updatedAt: 'desc' }],
      skip: (params.page - 1) * params.pageSize,
      take: params.pageSize,
    });
  }

  countManagedBindings(params: {
    mailboxId: string;
    keyword?: string;
    eventType?: string;
    status?: string;
  }) {
    const keyword = params.keyword?.trim();
    const where: Prisma.OutlookMeetingBindingWhereInput = {
      primaryMailboxId: params.mailboxId,
      manageStatus: {
        in: [OutlookManageStatus.MANAGED, OutlookManageStatus.SYNC_ERROR],
      },
      graphEventType: {
        in: ['single', 'seriesMaster'],
      },
    };
    if (keyword) {
      where.OR = [
        { graphEventId: { contains: keyword, mode: 'insensitive' } },
        { iCalUId: { contains: keyword, mode: 'insensitive' } },
        { meeting: { title: { contains: keyword, mode: 'insensitive' } } },
      ];
    }
    if (params.eventType) {
      where.graphEventType = params.eventType;
    }
    if (params.status) {
      where.meeting = {
        ...(where.meeting || {}),
        status: params.status as any,
      };
    }
    return this.prisma.outlookMeetingBinding.count({ where });
  }

  listAllManagedBindings(params: {
    keyword?: string;
    eventType?: string;
    status?: string;
    page: number;
    pageSize: number;
  }) {
    const keyword = params.keyword?.trim();
    const where: Prisma.OutlookMeetingBindingWhereInput = {
      manageStatus: {
        in: [OutlookManageStatus.MANAGED, OutlookManageStatus.SYNC_ERROR],
      },
      // Admin all-bindings page uses grouped display (root + lazy-loaded children),
      // so default pagination should be based on roots only.
      graphEventType: {
        in: ['single', 'seriesMaster'],
      },
    };
    if (keyword) {
      where.OR = [
        { graphEventId: { contains: keyword, mode: 'insensitive' } },
        { iCalUId: { contains: keyword, mode: 'insensitive' } },
        { meeting: { title: { contains: keyword, mode: 'insensitive' } } },
        { ownerEmail: { contains: keyword, mode: 'insensitive' } },
        { primaryMailbox: { mailboxEmail: { contains: keyword, mode: 'insensitive' } } },
      ];
    }
    if (params.eventType) {
      where.graphEventType = params.eventType;
    }
    if (params.status) {
      where.meeting = {
        ...(where.meeting || {}),
        status: params.status as any,
      };
    }

    return this.prisma.outlookMeetingBinding.findMany({
      where,
      include: {
        meeting: {
          select: {
            id: true,
            title: true,
            startTime: true,
            endTime: true,
            status: true,
            timezone: true,
          },
        },
        meetingSeries: {
          select: {
            id: true,
            title: true,
            startDate: true,
            timezone: true,
          },
        },
        primaryMailbox: {
          select: {
            id: true,
            mailboxEmail: true,
          },
        },
      },
      orderBy: [{ meeting: { startTime: 'asc' } }, { updatedAt: 'desc' }],
      skip: (params.page - 1) * params.pageSize,
      take: params.pageSize,
    });
  }

  countAllManagedBindings(params: { keyword?: string; eventType?: string; status?: string }) {
    const keyword = params.keyword?.trim();
    const where: Prisma.OutlookMeetingBindingWhereInput = {
      manageStatus: {
        in: [OutlookManageStatus.MANAGED, OutlookManageStatus.SYNC_ERROR],
      },
      graphEventType: {
        in: ['single', 'seriesMaster'],
      },
    };
    if (keyword) {
      where.OR = [
        { graphEventId: { contains: keyword, mode: 'insensitive' } },
        { iCalUId: { contains: keyword, mode: 'insensitive' } },
        { meeting: { title: { contains: keyword, mode: 'insensitive' } } },
        { ownerEmail: { contains: keyword, mode: 'insensitive' } },
        { primaryMailbox: { mailboxEmail: { contains: keyword, mode: 'insensitive' } } },
      ];
    }
    if (params.eventType) {
      where.graphEventType = params.eventType;
    }
    if (params.status) {
      where.meeting = {
        ...(where.meeting || {}),
        status: params.status as any,
      };
    }

    return this.prisma.outlookMeetingBinding.count({ where });
  }

  listManagedSeriesChildren(params: { seriesMasterId: string; mailboxId?: string }) {
    const where: Prisma.OutlookMeetingBindingWhereInput = {
      graphSeriesMasterId: params.seriesMasterId,
      graphEventId: { not: params.seriesMasterId },
      manageStatus: {
        in: [OutlookManageStatus.MANAGED, OutlookManageStatus.SYNC_ERROR],
      },
    };
    if (params.mailboxId) {
      where.primaryMailboxId = params.mailboxId;
    }
    return this.prisma.outlookMeetingBinding.findMany({
      where,
      include: {
        meeting: {
          select: {
            id: true,
            title: true,
            startTime: true,
            endTime: true,
            status: true,
            timezone: true,
          },
        },
        meetingSeries: {
          select: {
            id: true,
            title: true,
            startDate: true,
            timezone: true,
          },
        },
        primaryMailbox: {
          select: {
            id: true,
            mailboxEmail: true,
          },
        },
      },
      orderBy: [{ meeting: { startTime: 'asc' } }, { updatedAt: 'desc' }],
    });
  }

  getCursorByMailboxId(mailboxId: string) {
    return this.prisma.outlookSyncCursor.findUnique({
      where: { mailboxId },
    });
  }

  upsertCursor(mailboxId: string, deltaToken: string, fields?: {
    lastSyncedAt?: Date;
    lastReconciledAt?: Date;
  }) {
    return this.prisma.outlookSyncCursor.upsert({
      where: { mailboxId },
      create: {
        mailboxId,
        deltaToken,
        lastSyncedAt: fields?.lastSyncedAt,
        lastReconciledAt: fields?.lastReconciledAt,
      },
      update: {
        deltaToken,
        lastSyncedAt: fields?.lastSyncedAt,
        lastReconciledAt: fields?.lastReconciledAt,
      },
    });
  }

  findDefaultPrimaryMailbox() {
    return this.prisma.outlookSyncMailbox.findFirst({
      where: {
        isPrimaryDefault: true,
        isEnabled: true,
      },
      orderBy: { updatedAt: 'desc' },
    });
  }
}
