import { z } from 'zod'
import { FlightDetailStatus, VisaStatus } from '@prisma/client'
import { TRPCError } from '@trpc/server'
import { router, adminProcedure } from '../trpc'
import { logAudit } from '../utils/audit'
import { createNotification, NotificationTypes } from '../services/in-app-notification'

export const logisticsRouter = router({
  // ─────────────────────────────────────────────────────────────────────────
  // Hotels CRUD (multi-hotel per edition)
  // ─────────────────────────────────────────────────────────────────────────

  /** List all hotels for an edition, including occupancy count. */
  listHotels: adminProcedure
    .input(z.object({ programId: z.string() }))
    .query(async ({ ctx, input }) => {
      return ctx.prisma.hotel.findMany({
        where: { programId: input.programId },
        include: { _count: { select: { stays: true } } },
        orderBy: { name: 'asc' },
      })
    }),

  /** Create a new hotel for the edition. Empty link strings are stored as null. */
  createHotel: adminProcedure
    .input(
      z.object({
        programId: z.string(),
        name: z.string().min(1).max(200),
        address: z.string().max(500).optional(),
        link: z
          .string()
          .url()
          .or(z.literal(''))
          .optional(),
        notes: z.string().max(2000).optional(),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      const link = input.link && input.link.trim().length > 0 ? input.link : null
      const hotel = await ctx.prisma.hotel.create({
        data: {
          programId: input.programId,
          name: input.name,
          address: input.address ?? null,
          link,
          notes: input.notes ?? null,
        },
      })
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'HOTEL_CREATE',
        entityType: 'Hotel',
        entityId: hotel.id,
        detailsJson: { programId: input.programId, name: input.name },
      })
      return hotel
    }),

  /** Update an existing hotel's fields. Empty link strings are stored as null. */
  updateHotel: adminProcedure
    .input(
      z.object({
        id: z.string(),
        name: z.string().min(1).max(200),
        address: z.string().max(500).optional().nullable(),
        link: z
          .string()
          .url()
          .or(z.literal(''))
          .optional()
          .nullable(),
        notes: z.string().max(2000).optional().nullable(),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      const link =
        input.link !== undefined
          ? input.link && input.link.trim().length > 0
            ? input.link
            : null
          : undefined
      const hotel = await ctx.prisma.hotel.update({
        where: { id: input.id },
        data: {
          name: input.name,
          ...(input.address !== undefined ? { address: input.address } : {}),
          ...(link !== undefined ? { link } : {}),
          ...(input.notes !== undefined ? { notes: input.notes } : {}),
        },
      })
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'HOTEL_UPDATE',
        entityType: 'Hotel',
        entityId: hotel.id,
        detailsJson: { id: input.id, name: input.name },
      })
      return hotel
    }),

  /**
   * Delete a hotel. Rejected with BAD_REQUEST if any HotelStay rows reference it
   * (the user must reassign occupants first).
   */
  deleteHotel: adminProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const occupantCount = await ctx.prisma.hotelStay.count({
        where: { hotelId: input.id },
      })
      if (occupantCount > 0) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: `Reassign ${occupantCount} occupant(s) before deleting this hotel.`,
        })
      }
      const hotel = await ctx.prisma.hotel.delete({ where: { id: input.id } })
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'HOTEL_DELETE',
        entityType: 'Hotel',
        entityId: hotel.id,
        detailsJson: { id: input.id, name: hotel.name },
      })
      return { success: true }
    }),

  // ─────────────────────────────────────────────────────────────────────────
  // Rooming — per-attendee hotel/room assignment
  // ─────────────────────────────────────────────────────────────────────────

  /**
   * One row per CONFIRMED AttendingMember in the program.
   * Each row includes their hotelStay (null when not yet assigned).
   * Sorted by project title then user name.
   */
  listRooming: adminProcedure
    .input(z.object({ programId: z.string() }))
    .query(async ({ ctx, input }) => {
      const rows = await ctx.prisma.attendingMember.findMany({
        where: {
          confirmation: {
            status: 'CONFIRMED',
            project: { programId: input.programId },
          },
        },
        select: {
          id: true,
          user: { select: { id: true, name: true, email: true } },
          confirmation: {
            select: {
              id: true,
              project: { select: { id: true, title: true } },
            },
          },
          hotelStay: {
            select: { hotelId: true, roomNumber: true, checkInAt: true, checkOutAt: true },
          },
        },
        orderBy: [
          { confirmation: { project: { title: 'asc' } } },
          { user: { name: 'asc' } },
        ],
      })

      return rows.map((r) => ({
        attendingMemberId: r.id,
        confirmationId: r.confirmation.id,
        projectId: r.confirmation.project.id,
        projectTitle: r.confirmation.project.title,
        user: r.user,
        stay: r.hotelStay ?? null,
      }))
    }),

  /**
   * Upsert an individual attendee's hotel/room assignment.
   * Validates that the hotel belongs to the same program as the attendee.
   */
  assignStay: adminProcedure
    .input(
      z.object({
        attendingMemberId: z.string(),
        hotelId: z.string(),
        roomNumber: z.string().max(50).optional().nullable(),
        checkInAt: z.date().optional().nullable(),
        checkOutAt: z.date().optional().nullable(),
        notes: z.string().max(2000).optional().nullable(),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      // Validate hotel belongs to the same program as the attendee
      const [attendee, hotel] = await Promise.all([
        ctx.prisma.attendingMember.findUnique({
          where: { id: input.attendingMemberId },
          select: {
            confirmation: { select: { project: { select: { programId: true } } } },
          },
        }),
        ctx.prisma.hotel.findUnique({
          where: { id: input.hotelId },
          select: { programId: true },
        }),
      ])

      if (!attendee || !hotel) {
        throw new TRPCError({ code: 'NOT_FOUND', message: 'Attendee or hotel not found.' })
      }
      if (attendee.confirmation.project.programId !== hotel.programId) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'Hotel does not belong to the same program as the attendee.',
        })
      }

      const data: Record<string, unknown> = {
        hotelId: input.hotelId,
      }
      if (input.roomNumber !== undefined) data.roomNumber = input.roomNumber
      if (input.checkInAt !== undefined) data.checkInAt = input.checkInAt
      if (input.checkOutAt !== undefined) data.checkOutAt = input.checkOutAt
      if (input.notes !== undefined) data.notes = input.notes

      const stay = await ctx.prisma.hotelStay.upsert({
        where: { attendingMemberId: input.attendingMemberId },
        create: {
          attendingMemberId: input.attendingMemberId,
          hotelId: input.hotelId,
          roomNumber: input.roomNumber ?? null,
          checkInAt: input.checkInAt ?? null,
          checkOutAt: input.checkOutAt ?? null,
          notes: input.notes ?? null,
        },
        update: data,
      })

      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'HOTEL_STAY_ASSIGN',
        entityType: 'HotelStay',
        entityId: stay.id,
        detailsJson: { attendingMemberId: input.attendingMemberId, hotelId: input.hotelId },
      })
      return stay
    }),

  /**
   * Assign all AttendingMembers of a confirmation to a hotel (the "whole team" shortcut).
   * Preserves each member's existing roomNumber on update; sets it to null on create.
   * Validates the hotel belongs to the same program as the confirmation's project.
   */
  assignTeamToHotel: adminProcedure
    .input(
      z.object({
        confirmationId: z.string(),
        hotelId: z.string(),
        checkInAt: z.date().optional().nullable(),
        checkOutAt: z.date().optional().nullable(),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      // Validate program match
      const [confirmation, hotel] = await Promise.all([
        ctx.prisma.finalistConfirmation.findUnique({
          where: { id: input.confirmationId },
          select: {
            project: { select: { programId: true } },
            attendingMembers: { select: { id: true } },
          },
        }),
        ctx.prisma.hotel.findUnique({
          where: { id: input.hotelId },
          select: { programId: true },
        }),
      ])

      if (!confirmation || !hotel) {
        throw new TRPCError({ code: 'NOT_FOUND', message: 'Confirmation or hotel not found.' })
      }
      if (confirmation.project.programId !== hotel.programId) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'Hotel does not belong to the same program as this confirmation.',
        })
      }

      // Upsert each attendee's stay, preserving existing roomNumber
      const results = await Promise.all(
        confirmation.attendingMembers.map(async (member) => {
          const existing = await ctx.prisma.hotelStay.findUnique({
            where: { attendingMemberId: member.id },
            select: { roomNumber: true },
          })
          return ctx.prisma.hotelStay.upsert({
            where: { attendingMemberId: member.id },
            create: {
              attendingMemberId: member.id,
              hotelId: input.hotelId,
              roomNumber: null,
              checkInAt: input.checkInAt ?? null,
              checkOutAt: input.checkOutAt ?? null,
            },
            update: {
              hotelId: input.hotelId,
              ...(input.checkInAt !== undefined ? { checkInAt: input.checkInAt } : {}),
              ...(input.checkOutAt !== undefined ? { checkOutAt: input.checkOutAt } : {}),
              // Preserve existing roomNumber (do not overwrite it)
              ...(existing?.roomNumber !== undefined ? {} : {}),
            },
          })
        }),
      )

      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'HOTEL_TEAM_ASSIGN',
        entityType: 'FinalistConfirmation',
        entityId: input.confirmationId,
        detailsJson: {
          confirmationId: input.confirmationId,
          hotelId: input.hotelId,
          count: results.length,
        },
      })
      return { count: results.length }
    }),

  /** Remove an attendee's hotel/room assignment. No-op safe (deleteMany). */
  unassignStay: adminProcedure
    .input(z.object({ attendingMemberId: z.string() }))
    .mutation(async ({ ctx, input }) => {
      await ctx.prisma.hotelStay.deleteMany({
        where: { attendingMemberId: input.attendingMemberId },
      })
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'HOTEL_STAY_UNASSIGN',
        entityType: 'HotelStay',
        entityId: input.attendingMemberId,
        detailsJson: { attendingMemberId: input.attendingMemberId },
      })
      return { success: true }
    }),

  // ─────────────────────────────────────────────────────────────────────────
  // Confirmations
  // ─────────────────────────────────────────────────────────────────────────

  /**
   * Read-only listing of every FinalistConfirmation in a program, with the
   * joined project + attendee count + decline reason. Sorted by status
   * priority (PENDING first) then deadline ascending so the most urgent
   * decisions surface at the top of the table.
   */
  listConfirmations: adminProcedure
    .input(z.object({ programId: z.string() }))
    .query(async ({ ctx, input }) => {
      const rows = await ctx.prisma.finalistConfirmation.findMany({
        where: { project: { programId: input.programId } },
        include: {
          project: {
            select: { id: true, title: true, competitionCategory: true, country: true },
          },
          _count: { select: { attendingMembers: true } },
        },
      })
      const STATUS_PRIORITY: Record<string, number> = {
        PENDING: 0,
        CONFIRMED: 1,
        DECLINED: 2,
        EXPIRED: 3,
        SUPERSEDED: 4,
      }
      return rows
        .map((r) => ({
          id: r.id,
          status: r.status,
          deadline: r.deadline,
          confirmedAt: r.confirmedAt,
          declinedAt: r.declinedAt,
          declineReason: r.declineReason,
          expiredAt: r.expiredAt,
          category: r.category,
          promotedFromWaitlistEntryId: r.promotedFromWaitlistEntryId,
          project: r.project,
          attendeeCount: r._count.attendingMembers,
        }))
        .sort((a, b) => {
          const sa = STATUS_PRIORITY[a.status] ?? 9
          const sb = STATUS_PRIORITY[b.status] ?? 9
          if (sa !== sb) return sa - sb
          return a.deadline.getTime() - b.deadline.getTime()
        })
    }),

  // ─────────────────────────────────────────────────────────────────────────
  // Flight details
  // ─────────────────────────────────────────────────────────────────────────

  /**
   * List all attending members for CONFIRMED finalists in a program, with
   * their (optional) flight details. One row per attendee — even those
   * without a FlightDetail row yet, so the UI can render empty editors.
   */
  listFlightDetails: adminProcedure
    .input(z.object({ programId: z.string() }))
    .query(async ({ ctx, input }) => {
      return ctx.prisma.attendingMember.findMany({
        where: {
          confirmation: {
            status: 'CONFIRMED',
            project: { programId: input.programId },
          },
        },
        select: {
          id: true,
          needsVisa: true,
          user: { select: { id: true, name: true, email: true, country: true } },
          confirmation: {
            select: {
              project: {
                select: {
                  id: true,
                  title: true,
                  country: true,
                  competitionCategory: true,
                },
              },
            },
          },
          flightDetail: true,
        },
        orderBy: [{ user: { name: 'asc' } }],
      })
    }),

  /** Create or update a flight detail row for an attending member. */
  upsertFlightDetail: adminProcedure
    .input(
      z.object({
        attendingMemberId: z.string(),
        arrivalAt: z.date().nullable().optional(),
        arrivalFlightNumber: z.string().max(20).nullable().optional(),
        arrivalAirport: z.string().max(10).nullable().optional(),
        departureAt: z.date().nullable().optional(),
        departureFlightNumber: z.string().max(20).nullable().optional(),
        departureAirport: z.string().max(10).nullable().optional(),
        adminNotes: z.string().max(1000).nullable().optional(),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      const { attendingMemberId, ...rest } = input
      // Strip out undefineds so an upsert update doesn't blow away unset fields.
      const data: Record<string, unknown> = {}
      for (const [k, v] of Object.entries(rest)) {
        if (v !== undefined) data[k] = v
      }
      if (input.arrivalAt && input.departureAt && input.departureAt < input.arrivalAt) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'Departure must be after arrival',
        })
      }
      const detail = await ctx.prisma.flightDetail.upsert({
        where: { attendingMemberId },
        create: { attendingMemberId, ...(data as object) },
        update: data,
      })
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'FLIGHT_DETAIL_UPSERT',
        entityType: 'FlightDetail',
        entityId: detail.id,
        detailsJson: { attendingMemberId },
      })
      return detail
    }),

  /** Toggle PENDING ↔ CONFIRMED on a flight detail. */
  setFlightStatus: adminProcedure
    .input(
      z.object({
        flightDetailId: z.string(),
        status: z.nativeEnum(FlightDetailStatus),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      const detail = await ctx.prisma.flightDetail.update({
        where: { id: input.flightDetailId },
        data: { status: input.status },
      })
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'FLIGHT_STATUS_SET',
        entityType: 'FlightDetail',
        entityId: detail.id,
        detailsJson: { status: input.status },
      })

      // Send travel-confirmed notification to the attendee (best-effort — never throws)
      if (input.status === 'CONFIRMED') {
        try {
          const attendee = await ctx.prisma.attendingMember.findUnique({
            where: { id: detail.attendingMemberId },
            select: {
              userId: true,
              hotelStay: {
                include: { hotel: { select: { name: true, address: true, link: true } } },
              },
              confirmation: {
                select: {
                  project: {
                    select: {
                      title: true,
                    },
                  },
                },
              },
            },
          })
          if (attendee) {
            const projectTitle = attendee.confirmation.project.title
            const hotelStay = attendee.hotelStay
            await createNotification({
              userId: attendee.userId,
              type: NotificationTypes.TRAVEL_CONFIRMED,
              title: 'Your grand-finale travel is confirmed',
              message: 'Your flight details for the grand finale are confirmed.',
              linkUrl: '/applicant',
              metadata: {
                projectTitle,
                arrivalAt: detail.arrivalAt?.toISOString() ?? null,
                arrivalFlightNumber: detail.arrivalFlightNumber ?? null,
                arrivalAirport: detail.arrivalAirport ?? null,
                departureAt: detail.departureAt?.toISOString() ?? null,
                departureFlightNumber: detail.departureFlightNumber ?? null,
                departureAirport: detail.departureAirport ?? null,
                hotel: hotelStay?.hotel
                  ? {
                      name: hotelStay.hotel.name,
                      address: hotelStay.hotel.address ?? null,
                      link: hotelStay.hotel.link ?? null,
                    }
                  : null,
                roomNumber: hotelStay?.roomNumber ?? null,
                checkInAt: hotelStay?.checkInAt?.toISOString() ?? null,
                checkOutAt: hotelStay?.checkOutAt?.toISOString() ?? null,
              },
            })
          }
        } catch (err) {
          console.error('[setFlightStatus] Failed to send TRAVEL_CONFIRMED notification:', err)
        }
      }

      return detail
    }),

  // ─────────────────────────────────────────────────────────────────────────
  // Visa applications
  // ─────────────────────────────────────────────────────────────────────────

  /**
   * List all VisaApplication rows for a program, joined with the project +
   * attendee + project so the admin Visas tab can render a flat table.
   * Sorted by status priority (REQUESTED first → resolved last) so the most
   * urgent in-flight applications surface at the top.
   */
  listVisaApplications: adminProcedure
    .input(z.object({ programId: z.string() }))
    .query(async ({ ctx, input }) => {
      const rows = await ctx.prisma.visaApplication.findMany({
        where: {
          attendingMember: {
            confirmation: { project: { programId: input.programId } },
          },
        },
        include: {
          attendingMember: {
            include: {
              user: { select: { id: true, name: true, email: true } },
              confirmation: {
                select: {
                  id: true,
                  project: { select: { id: true, title: true } },
                },
              },
            },
          },
        },
      })
      const STATUS_PRIORITY: Record<VisaStatus, number> = {
        REQUESTED: 0,
        INVITATION_SENT: 1,
        APPOINTMENT_BOOKED: 2,
        GRANTED: 3,
        DENIED: 4,
        NOT_NEEDED: 5,
      }
      return rows
        .map((r) => ({
          id: r.id,
          status: r.status,
          nationality: r.nationality,
          invitationSentAt: r.invitationSentAt,
          appointmentAt: r.appointmentAt,
          decisionAt: r.decisionAt,
          notes: r.notes,
          updatedAt: r.updatedAt,
          attendee: {
            id: r.attendingMember.id,
            user: r.attendingMember.user,
          },
          project: r.attendingMember.confirmation.project,
        }))
        .sort((a, b) => {
          const sa = STATUS_PRIORITY[a.status] ?? 9
          const sb = STATUS_PRIORITY[b.status] ?? 9
          if (sa !== sb) return sa - sb
          return a.project.title.localeCompare(b.project.title)
        })
    }),

  /**
   * Update a VisaApplication's status, dates, nationality, and notes. Empty
   * date fields clear the value. Audit-logged as VISA_UPDATE.
   */
  updateVisaApplication: adminProcedure
    .input(
      z.object({
        id: z.string(),
        status: z.nativeEnum(VisaStatus).optional(),
        nationality: z.string().max(100).optional().nullable(),
        invitationSentAt: z.date().optional().nullable(),
        appointmentAt: z.date().optional().nullable(),
        decisionAt: z.date().optional().nullable(),
        notes: z.string().max(2000).optional().nullable(),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      const existing = await ctx.prisma.visaApplication.findUnique({
        where: { id: input.id },
        include: {
          attendingMember: {
            select: {
              userId: true,
              confirmation: {
                select: {
                  project: { select: { title: true } },
                },
              },
            },
          },
        },
      })
      if (!existing) {
        throw new TRPCError({ code: 'NOT_FOUND', message: 'Visa application not found' })
      }
      const data: Record<string, unknown> = {}
      if (input.status !== undefined) data.status = input.status
      if (input.nationality !== undefined) data.nationality = input.nationality
      if (input.invitationSentAt !== undefined) data.invitationSentAt = input.invitationSentAt
      if (input.appointmentAt !== undefined) data.appointmentAt = input.appointmentAt
      if (input.decisionAt !== undefined) data.decisionAt = input.decisionAt
      if (input.notes !== undefined) data.notes = input.notes
      const updated = await ctx.prisma.visaApplication.update({
        where: { id: input.id },
        data,
      })
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'VISA_UPDATE',
        entityType: 'VisaApplication',
        entityId: updated.id,
        detailsJson: {
          previous: {
            status: existing.status,
            nationality: existing.nationality,
            invitationSentAt: existing.invitationSentAt,
            appointmentAt: existing.appointmentAt,
            decisionAt: existing.decisionAt,
          },
          next: data,
        },
      })

      // Send visa-status notification to the attendee when a notifiable status changes (best-effort)
      const NOTIFIABLE_STATUSES = new Set<VisaStatus>([
        'INVITATION_SENT',
        'APPOINTMENT_BOOKED',
        'GRANTED',
        'DENIED',
      ])
      if (
        input.status !== undefined &&
        input.status !== existing.status &&
        NOTIFIABLE_STATUSES.has(input.status)
      ) {
        try {
          const projectTitle = existing.attendingMember.confirmation.project.title
          const statusMessages: Record<string, string> = {
            INVITATION_SENT: 'Your visa invitation letter for the Grand Finale has been sent.',
            APPOINTMENT_BOOKED: 'A visa appointment has been booked for you.',
            GRANTED: 'Congratulations — your visa for the Grand Finale has been granted!',
            DENIED: 'Your visa application outcome is now available. Please contact the MOPC team.',
          }
          await createNotification({
            userId: existing.attendingMember.userId,
            type: NotificationTypes.VISA_STATUS_UPDATE,
            title: 'Visa update for the grand finale',
            message: statusMessages[input.status] ?? 'Your visa status has been updated.',
            linkUrl: '/applicant',
            metadata: {
              projectTitle,
              status: input.status,
            },
          })
        } catch (err) {
          console.error('[updateVisaApplication] Failed to send VISA_STATUS_UPDATE notification:', err)
        }
      }

      return updated
    }),

  /** Read Program.visaStatusVisibleToMembers — drives the admin Visas tab toggle. */
  getVisaVisibility: adminProcedure
    .input(z.object({ programId: z.string() }))
    .query(async ({ ctx, input }) => {
      const program = await ctx.prisma.program.findUniqueOrThrow({
        where: { id: input.programId },
        select: { visaStatusVisibleToMembers: true },
      })
      return { visible: program.visaStatusVisibleToMembers }
    }),

  /**
   * Flip Program.visaStatusVisibleToMembers. Controls whether the team can
   * see their own visa status on the applicant dashboard.
   */
  setVisaVisibility: adminProcedure
    .input(z.object({ programId: z.string(), visible: z.boolean() }))
    .mutation(async ({ ctx, input }) => {
      const program = await ctx.prisma.program.update({
        where: { id: input.programId },
        data: { visaStatusVisibleToMembers: input.visible },
      })
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'VISA_VISIBILITY_SET',
        entityType: 'Program',
        entityId: program.id,
        detailsJson: { visible: input.visible },
      })
      return { visible: program.visaStatusVisibleToMembers }
    }),
})
