import jwt from 'jsonwebtoken';
import { AppDataSource } from '../models/index';
import { BookingRepository } from '../models/repositories/BookingRepository';
import { EventTypeRepository } from '../models/repositories/EventTypeRepository';
import { UserRepository } from '../models/repositories/UserRepository';
import { PublicService } from './PublicService';
import { AvailabilityService } from './AvailabilityService';
import { AuditLogRepository } from '../models/repositories/AuditLogRepository';
import { AppError } from '../utils/AppError';
import { hashSha256 } from '../utils/crypto';
import { generateUuid } from '../utils/uuid';
import { addMinutes } from '../utils/timezone';
import { env } from '../config/env';
import { Booking } from '../models/entities/Booking.entity';
import { EventType } from '../models/entities/EventType.entity';
import { User } from '../models/entities/User.entity';
import { enqueueNotification, enqueueCalendarSync } from '../jobs/queues';
import { SlotView } from '../views/SlotView';

export class BookingService {
  constructor(
    private bookingRepo = new BookingRepository(),
    private publicService = new PublicService(),
    private availabilityService = new AvailabilityService(),
    private auditRepo = new AuditLogRepository(),
    private eventTypeRepo = new EventTypeRepository(),
    private userRepo = new UserRepository(),
  ) {}

  buildCancelUrl(cancelToken: string): string {
    return `${env.bookingAppUrl}/cancel/${cancelToken}`;
  }

  private createCancelToken(bookingId: string): { token: string; hash: string } {
    const token = jwt.sign({ bookingId, type: 'cancel' }, env.JWT_SECRET, { expiresIn: '30d' });
    return { token, hash: hashSha256(token) };
  }

  async createPublicBooking(input: {
    username: string;
    eventSlug: string;
    startTime: string;
    guestName: string;
    guestEmail: string;
    guestTimezone: string;
    customAnswers?: Record<string, string>;
    idempotencyKey?: string;
    embedSource?: string;
  }): Promise<Booking & { cancelUrl: string; cancelToken: string }> {
    if (input.idempotencyKey) {
      const existing = await this.bookingRepo.findByIdempotencyKey(input.idempotencyKey);
      if (existing) {
        const cancelToken = jwt.sign({ bookingId: existing.id, type: 'cancel' }, env.JWT_SECRET, {
          expiresIn: '30d',
        });
        return {
          ...existing,
          cancelUrl: this.buildCancelUrl(cancelToken),
          cancelToken,
        };
      }
    }

    const { user, eventType } = await this.publicService.getEventPage(input.username, input.eventSlug);
    return this.createConfirmedBooking({
      user,
      eventType,
      startTime: input.startTime,
      guestName: input.guestName,
      guestEmail: input.guestEmail,
      guestTimezone: input.guestTimezone,
      customAnswers: input.customAnswers,
      idempotencyKey: input.idempotencyKey,
      embedSource: input.embedSource,
    });
  }

  async createForHost(
    userId: string,
    input: {
      eventTypeId?: string;
      startTime: string;
      guestName: string;
      guestEmail: string;
      guestTimezone: string;
      customAnswers?: Record<string, string>;
      idempotencyKey?: string;
    },
  ): Promise<Booking & { cancelUrl: string; cancelToken: string }> {
    if (input.idempotencyKey) {
      const existing = await this.bookingRepo.findByIdempotencyKey(input.idempotencyKey);
      if (existing) {
        const cancelToken = jwt.sign({ bookingId: existing.id, type: 'cancel' }, env.JWT_SECRET, {
          expiresIn: '30d',
        });
        return {
          ...existing,
          cancelUrl: this.buildCancelUrl(cancelToken),
          cancelToken,
        };
      }
    }

    const eventType = await this.resolveEventTypeForHost(userId, input.eventTypeId);
    const user = await this.userRepo.findById(userId);
    if (!user) throw new AppError('not_found', 404, 'User not found.');

    const start = new Date(input.startTime);

    try {
      return await this.createConfirmedBooking({
        user,
        eventType,
        startTime: input.startTime,
        guestName: input.guestName,
        guestEmail: input.guestEmail,
        guestTimezone: input.guestTimezone,
        customAnswers: input.customAnswers,
        idempotencyKey: input.idempotencyKey,
      });
    } catch (err) {
      if (err instanceof AppError && err.code === 'slot_unavailable') {
        throw await this.slotUnavailableWithTimes({
          userId,
          eventTypeId: eventType.id,
          start,
          guestTimezone: input.guestTimezone,
        });
      }
      throw err;
    }
  }

  private async resolveEventTypeForHost(userId: string, eventTypeId?: string): Promise<EventType> {
    if (eventTypeId) {
      const eventType = await this.eventTypeRepo.findById(eventTypeId);
      if (!eventType || eventType.userId !== userId || !eventType.isActive) {
        throw new AppError('not_found', 404, 'Event type not found.');
      }
      return eventType;
    }

    const active = await this.eventTypeRepo.findByUserId(userId, true);
    if (active.length === 0) {
      throw new AppError('not_found', 404, 'No active event type found for this host.');
    }
    if (active.length > 1) {
      throw new AppError(
        'validation_error',
        400,
        'Host has multiple event types. Pass eventTypeId in the body.',
        { eventTypeIds: active.map((e) => e.id) },
      );
    }
    return active[0];
  }

  private async slotUnavailableWithTimes(params: {
    userId: string;
    eventTypeId: string;
    start: Date;
    guestTimezone: string;
  }): Promise<AppError> {
    let availableTimes: ReturnType<typeof SlotView.toJson>[] = [];
    try {
      const slots = await this.availabilityService.findOpenTimesAfter({
        userId: params.userId,
        eventTypeId: params.eventTypeId,
        after: params.start,
        guestTimezone: params.guestTimezone,
      });
      availableTimes = slots.map(SlotView.toJson);
    } catch {
      availableTimes = [];
    }

    return new AppError(
      'slot_unavailable',
      409,
      'This time slot is not available. Choose one of the availableTimes.',
      {
        created: false,
        requestedStartTime: params.start.toISOString(),
        availableTimes,
        alternatives: availableTimes,
      },
    );
  }

  private async createConfirmedBooking(input: {
    user: User;
    eventType: EventType;
    startTime: string;
    guestName: string;
    guestEmail: string;
    guestTimezone: string;
    customAnswers?: Record<string, string>;
    idempotencyKey?: string;
    embedSource?: string;
  }): Promise<Booking & { cancelUrl: string; cancelToken: string }> {
    const { user, eventType } = input;
    const start = new Date(input.startTime);
    const end = addMinutes(start, eventType.durationMinutes);

    const { hash: cancelTokenHash } = this.createCancelToken(generateUuid());

    const booking = await AppDataSource.transaction(async (manager) => {
      const overlaps = await this.bookingRepo.findOverlapping(user.id, start, end, manager);
      if (overlaps.length > 0) {
        throw new AppError('slot_unavailable', 409, 'This time slot is no longer available. Please choose another.');
      }

      const withinHours = await this.availabilityService.isWithinAvailableHours(
        user.id,
        start,
        end,
        eventType,
      );
      if (!withinHours) {
        throw new AppError('slot_unavailable', 409, 'This time slot is no longer available. Please choose another.');
      }

      const busy = await this.availabilityService.isSlotBusy(user.id, start, end, eventType);
      if (busy) {
        throw new AppError('slot_unavailable', 409, 'This time slot is no longer available. Please choose another.');
      }

      const created = await this.bookingRepo.create(
        {
          id: generateUuid(),
          userId: user.id,
          eventTypeId: eventType.id,
          guestName: input.guestName,
          guestEmail: input.guestEmail,
          guestTimezone: input.guestTimezone,
          startTime: start,
          endTime: end,
          customAnswers: input.customAnswers ?? {},
          status: 'confirmed',
          cancelTokenHash,
          idempotencyKey: input.idempotencyKey ?? null,
          embedSource: input.embedSource ?? null,
        },
        manager,
      );

      created.eventType = eventType;
      created.user = user;
      return created;
    });

    const finalCancelToken = jwt.sign({ bookingId: booking.id, type: 'cancel' }, env.JWT_SECRET, {
      expiresIn: '30d',
    });
    await this.bookingRepo.update(booking.id, { cancelTokenHash: hashSha256(finalCancelToken) });

    await this.auditRepo.log({
      userId: user.id,
      action: 'booking.create',
      entityType: 'booking',
      entityId: booking.id,
      metadata: { guestEmail: input.guestEmail, embedSource: input.embedSource },
    });

    await enqueueNotification('sendConfirmationEmail', {
      bookingId: booking.id,
      cancelToken: finalCancelToken,
    });
    await enqueueCalendarSync('pushCalendarEvent', { bookingId: booking.id });

    return {
      ...booking,
      cancelUrl: this.buildCancelUrl(finalCancelToken),
      cancelToken: finalCancelToken,
    };
  }

  async cancelByGuest(token: string): Promise<Booking> {
    let payload: { bookingId: string; type: string };
    try {
      payload = jwt.verify(token, env.JWT_SECRET) as { bookingId: string; type: string };
    } catch {
      throw new AppError('validation_error', 400, 'Invalid or expired cancel link.');
    }

    if (payload.type !== 'cancel') {
      throw new AppError('validation_error', 400, 'Invalid cancel token.');
    }

    const booking = await this.bookingRepo.findById(payload.bookingId);
    if (!booking) throw new AppError('not_found', 404, 'Booking not found.');
    if (booking.status === 'cancelled') return booking;

    if (hashSha256(token) !== booking.cancelTokenHash) {
      throw new AppError('validation_error', 400, 'Invalid cancel token.');
    }

    return this.cancelBooking(booking, 'guest');
  }

  async cancelByHost(userId: string, bookingId: string, reason?: string): Promise<Booking> {
    const booking = await this.bookingRepo.findById(bookingId);
    if (!booking || booking.userId !== userId) {
      throw new AppError('not_found', 404, 'Booking not found.');
    }
    return this.cancelBooking(booking, 'host', reason);
  }

  private async cancelBooking(
    booking: Booking,
    cancelledBy: 'guest' | 'host',
    reason?: string,
  ): Promise<Booking> {
    if (booking.status === 'cancelled') return booking;

    const updated = await this.bookingRepo.update(booking.id, {
      status: 'cancelled',
      cancelledAt: new Date(),
      cancellationReason: reason ?? `Cancelled by ${cancelledBy}`,
    });

    await this.auditRepo.log({
      userId: booking.userId,
      action: 'booking.cancel',
      entityType: 'booking',
      entityId: booking.id,
      metadata: { cancelledBy },
    });

    await enqueueNotification('sendCancellationEmail', {
      bookingId: booking.id,
      cancelledBy,
    });
    await enqueueCalendarSync('deleteCalendarEvent', { bookingId: booking.id });

    return updated;
  }

  async reschedule(userId: string, bookingId: string, startTime: string): Promise<Booking> {
    const booking = await this.bookingRepo.findById(bookingId);
    if (!booking || booking.userId !== userId) {
      throw new AppError('not_found', 404, 'Booking not found.');
    }
    if (booking.status !== 'confirmed') {
      throw new AppError('validation_error', 400, 'Only confirmed bookings can be rescheduled.');
    }

    const eventType = booking.eventType;
    const start = new Date(startTime);
    const end = addMinutes(start, eventType.durationMinutes);

    await AppDataSource.transaction(async (manager) => {
      const overlaps = await this.bookingRepo.findOverlapping(
        userId,
        start,
        end,
        manager,
        bookingId,
      );
      if (overlaps.length > 0) {
        throw new AppError('slot_unavailable', 409, 'This time slot is no longer available.');
      }

      const withinHours = await this.availabilityService.isWithinAvailableHours(
        userId,
        start,
        end,
        eventType,
      );
      if (!withinHours) {
        throw new AppError('slot_unavailable', 409, 'This time slot is no longer available.');
      }

      const busy = await this.availabilityService.isSlotBusy(userId, start, end, eventType);
      if (busy) {
        throw new AppError('slot_unavailable', 409, 'This time slot is no longer available.');
      }

      await this.bookingRepo.update(bookingId, { startTime: start, endTime: end }, manager);
    });

    const updated = await this.bookingRepo.findById(bookingId);
    if (!updated) throw new AppError('not_found', 404, 'Booking not found.');

    await enqueueCalendarSync('updateCalendarEvent', { bookingId });
    await enqueueNotification('sendRescheduleEmail', { bookingId });

    return updated;
  }

  async listForHost(
    userId: string,
    filters: { status?: Booking['status']; from?: string; to?: string; page?: number; limit?: number },
  ) {
    const page = filters.page ?? 1;
    const limit = filters.limit ?? 20;
    const [bookings, total] = await this.bookingRepo.findByUserInRange(
      userId,
      filters.from ? new Date(filters.from) : undefined,
      filters.to ? new Date(`${filters.to}T23:59:59.999Z`) : undefined,
      filters.status,
      page,
      limit,
    );
    return { bookings, pagination: { page, limit, total } };
  }

  async updateMeetingOutcome(
    userId: string,
    bookingId: string,
    input: { meetingOutcome: 'held' | 'no_show' | 'unknown'; hostRating?: number | null },
  ): Promise<Booking> {
    const booking = await this.bookingRepo.findById(bookingId);
    if (!booking || booking.userId !== userId) {
      throw new AppError('not_found', 404, 'Booking not found.');
    }
    if (booking.status !== 'confirmed') {
      throw new AppError('validation_error', 400, 'Only confirmed bookings can be updated.');
    }

    return this.bookingRepo.update(bookingId, {
      meetingOutcome: input.meetingOutcome,
      hostRating: input.hostRating ?? booking.hostRating,
      meetingOutcomeAt: new Date(),
    });
  }

  async submitGuestFeedback(input: {
    token: string;
    guestRating: number;
    guestFeedback?: string | null;
  }): Promise<Booking> {
    const { verifySurveyToken } = await import('../utils/surveyToken');
    const bookingId = verifySurveyToken(input.token);
    const booking = await this.bookingRepo.findById(bookingId);
    if (!booking || booking.status !== 'confirmed') {
      throw new AppError('not_found', 404, 'Booking not found.');
    }

    return this.bookingRepo.update(bookingId, {
      guestRating: input.guestRating,
      guestFeedback: input.guestFeedback ?? null,
      meetingOutcome: booking.meetingOutcome === 'pending' ? 'held' : booking.meetingOutcome,
      meetingOutcomeAt: booking.meetingOutcomeAt ?? new Date(),
    });
  }
}
