import {
  HttpStatus,
  Injectable,
} from '@nestjs/common';
import { AppException, ErrorCode } from '../../common/errors';
import * as bcrypt from 'bcrypt';
import { createHash, randomBytes } from 'crypto';
import { Prisma, UserNotificationType } from '@prisma/client';
import { PrismaService } from '../../../prisma/prisma.service';
import { AcceptStaffInviteDto } from './dto/accept-staff-invite.dto';
import { isUnlimitedSeats, normalizeTabPermissions } from '../../common/permissions';
import {
  filterPermissionsForOrgType,
  getOrganizationTypeName,
} from '../../common/organization-type';
import { InviteStaffDto } from './dto/invite-staff.dto';
import { UpdateStaffMemberDto } from './dto/update-staff-member.dto';
import { UserNotificationService } from '../notifications/user-notification.service';

@Injectable()
export class StaffService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly userNotifications: UserNotificationService,
  ) {}

  getOrganizationIdFromUser(user: { organizationId?: string }) {
    if (!user?.organizationId) {
      throw new AppException(ErrorCode.AUTH_ORG_NOT_SELECTED, HttpStatus.BAD_REQUEST);
    }
    return user.organizationId;
  }

  async list(userId: string, organizationId: string) {
    const actor = await this.getActorMembership(userId, organizationId);
    if (!actor || !this.canViewStaff(actor)) {
      throw new AppException(ErrorCode.PERMISSION_ACCESS_STAFF, HttpStatus.FORBIDDEN);
    }

    const org = await this.prisma.organization.findUnique({
      where: { id: organizationId },
      include: { plan: true },
    });
    if (!org) {
      throw new AppException(ErrorCode.PERMISSION_ORG_NOT_FOUND, HttpStatus.NOT_FOUND);
    }

    const [members, seatsUsed] = await Promise.all([
      this.prisma.membership.findMany({
        where: { organizationId },
        include: {
          user: { select: { id: true, email: true, name: true, passwordHash: true } },
          permissions: { include: { permission: true } },
          invitations: {
            orderBy: { createdAt: 'desc' },
            take: 1,
          },
        },
        orderBy: [{ isOwner: 'desc' }, { createdAt: 'asc' }],
      }),
      this.prisma.membership.count({
        where: {
          organizationId,
          OR: [{ isOwner: true }, { isActive: true }],
        },
      }),
    ]);

    const maxUsers = org.plan?.maxUsers ?? 0;
    const unlimited = isUnlimitedSeats(maxUsers);

    return {
      success: true,
      data: {
        members: members.map((m) => ({
          id: m.id,
          userId: m.user.id,
          email: m.user.email,
          name: m.user.name,
          isOwner: m.isOwner,
          isActive: m.isOwner ? true : m.isActive,
          invitationStatus: this.getInvitationStatus(m),
          hasPassword: Boolean(m.user.passwordHash),
          invitedAt: m.invitations[0]?.createdAt?.toISOString() || null,
          acceptedAt: m.invitations[0]?.acceptedAt?.toISOString() || null,
          permissions: m.isOwner
            ? null
            : m.permissions.map((p) => p.permission.name),
        })),
        seats: {
          used: seatsUsed,
          limit: unlimited ? null : maxUsers,
          unlimited,
        },
      },
    };
  }

  async invite(userId: string, organizationId: string, dto: InviteStaffDto) {
    const actor = await this.getActorMembership(userId, organizationId);
    if (!actor || !this.canEditStaff(actor)) {
      throw new AppException(ErrorCode.PERMISSION_EDIT_STAFF, HttpStatus.FORBIDDEN);
    }

    const email = dto.email.trim().toLowerCase();
    const orgType = await getOrganizationTypeName(this.prisma, organizationId);
    const normalizedPerms = filterPermissionsForOrgType(dto.permissionNames, orgType);

    const permissionRows = await this.prisma.permission.findMany({
      where: { name: { in: normalizedPerms } },
      select: { id: true, name: true },
    });
    if (permissionRows.length !== normalizedPerms.length) {
      const ok = new Set(permissionRows.map((p) => p.name));
      const missing = normalizedPerms.filter((n) => !ok.has(n));
      throw new AppException(ErrorCode.STAFF_UNKNOWN_PERMISSIONS, HttpStatus.BAD_REQUEST);
    }

    const plainToken = this.generateInviteToken();
    const tokenHash = this.hashInviteToken(plainToken);

    const result = await this.prisma.$transaction(async (tx) => {
      const org = await tx.organization.findUnique({
        where: { id: organizationId },
        include: { plan: true },
      });
      if (!org) {
        throw new AppException(ErrorCode.PERMISSION_ORG_NOT_FOUND, HttpStatus.NOT_FOUND);
      }

      if (!org.plan) {
        throw new AppException(ErrorCode.STAFF_NO_SUBSCRIPTION, HttpStatus.BAD_REQUEST);
      }

      const maxUsers = org.plan.maxUsers;
      const seatsUsed = await tx.membership.count({
        where: {
          organizationId,
          OR: [{ isOwner: true }, { isActive: true }],
        },
      });
      if (!isUnlimitedSeats(maxUsers) && seatsUsed >= maxUsers) {
        throw new AppException(ErrorCode.STAFF_SEAT_LIMIT, HttpStatus.BAD_REQUEST);
      }

      const existingUser = await tx.user.findUnique({ where: { email } });
      let targetUserId: string;

      if (existingUser) {
        if (existingUser.id === org.ownerId) {
          throw new AppException(ErrorCode.STAFF_INVITE_OWNER_EMAIL, HttpStatus.BAD_REQUEST);
        }
        const dup = await tx.membership.findUnique({
          where: {
            userId_organizationId: {
              userId: existingUser.id,
              organizationId,
            },
          },
        });
        if (dup) {
          throw new AppException(ErrorCode.STAFF_ALREADY_MEMBER, HttpStatus.CONFLICT);
        }
        targetUserId = existingUser.id;
      } else {
        const created = await tx.user.create({
          data: {
            email,
            name: dto.name.trim(),
            passwordHash: null,
          },
        });
        targetUserId = created.id;
      }

      const membership = await tx.membership.create({
        data: {
          userId: targetUserId,
          organizationId,
          isOwner: false,
          isActive: existingUser ? true : false,
        },
      });

      if (permissionRows.length > 0) {
        await tx.membershipPermission.createMany({
          data: permissionRows.map((p) => ({
            membershipId: membership.id,
            permissionId: p.id,
          })),
        });
      }

      let inviteUrl: string | null = null;
      let invitationId: string | null = null;

      if (!existingUser) {
        const invitation = await tx.staffInvitation.create({
          data: {
            membershipId: membership.id,
            invitedById: userId,
            tokenHash,
            expiresAt: this.getInviteExpiryDate(),
          },
        });
        invitationId = invitation.id;
        inviteUrl = this.buildInviteUrl(plainToken);
      }

      return {
        membershipId: membership.id,
        userId: targetUserId,
        invitationId,
        inviteUrl,
        isPending: !existingUser,
      };
    });

    void this.userNotifications.notify({
      organizationId,
      type: UserNotificationType.STAFF_INVITE,
      href: '/staff',
      actorUserId: userId,
      payload: {
        membershipId: result.membershipId,
        staffInvitationId: result.invitationId,
        email,
      },
      requiredPermission: 'TAB_STAFF_READ',
    });

    return {
      success: true,
      data: {
        membershipId: result.membershipId,
        userId: result.userId,
        email,
        invitationId: result.invitationId,
        invitationUrl: result.inviteUrl,
        invitationStatus: result.isPending ? 'PENDING' : 'ACCEPTED',
      },
    };
  }

  async getInvitationLink(userId: string, organizationId: string, membershipId: string) {
    const actor = await this.getActorMembership(userId, organizationId);
    if (!actor || !this.canEditStaff(actor)) {
      throw new AppException(ErrorCode.PERMISSION_EDIT_STAFF, HttpStatus.FORBIDDEN);
    }

    const membership = await this.prisma.membership.findFirst({
      where: { id: membershipId, organizationId },
      include: {
        user: { select: { email: true } },
        invitations: { orderBy: { createdAt: 'desc' }, take: 1 },
      },
    });

    if (!membership) {
      throw new AppException(ErrorCode.STAFF_MEMBER_NOT_FOUND, HttpStatus.NOT_FOUND);
    }
    if (membership.isOwner) {
      throw new AppException(ErrorCode.STAFF_OWNER_NO_INVITE_LINK, HttpStatus.BAD_REQUEST);
    }
    if (membership.isActive) {
      throw new AppException(ErrorCode.STAFF_ALREADY_ACCEPTED, HttpStatus.BAD_REQUEST);
    }

    const invitation = membership.invitations[0];
    if (!invitation) {
      throw new AppException(ErrorCode.STAFF_INVITE_MISSING, HttpStatus.BAD_REQUEST);
    }
    if (invitation.acceptedAt) {
      throw new AppException(ErrorCode.STAFF_INVITE_ALREADY_ACCEPTED, HttpStatus.BAD_REQUEST);
    }
    if (invitation.revokedAt) {
      throw new AppException(ErrorCode.STAFF_INVITE_INVALID, HttpStatus.BAD_REQUEST);
    }

    const plainToken = this.generateInviteToken();
    const tokenHash = this.hashInviteToken(plainToken);
    await this.prisma.staffInvitation.update({
      where: { id: invitation.id },
      data: {
        tokenHash,
        expiresAt: this.getInviteExpiryDate(),
      },
    });

    return {
      success: true,
      data: {
        membershipId: membership.id,
        invitationId: invitation.id,
        email: membership.user.email,
        invitationUrl: this.buildInviteUrl(plainToken),
      },
    };
  }

  async previewInvite(token: string) {
    const invitation = await this.findValidInvitation(token);
    const org = invitation.membership.organization;
    const user = invitation.membership.user;

    return {
      success: true,
      data: {
        email: user.email,
        name: user.name,
        organizationName: org.name,
        expiresAt: invitation.expiresAt.toISOString(),
        status: invitation.acceptedAt ? 'ACCEPTED' : 'PENDING',
        mode: invitation.membership.isActive ? 'password_setup' : 'join',
      },
    };
  }

  async clearPassword(
    actorUserId: string,
    organizationId: string,
    membershipId: string,
  ) {
    const actor = await this.getActorMembership(actorUserId, organizationId);
    if (!actor || !this.canEditStaff(actor)) {
      throw new AppException(ErrorCode.PERMISSION_EDIT_STAFF, HttpStatus.FORBIDDEN);
    }

    const membership = await this.prisma.membership.findFirst({
      where: { id: membershipId, organizationId },
      include: {
        user: { select: { id: true, email: true } },
      },
    });

    if (!membership) {
      throw new AppException(ErrorCode.STAFF_MEMBER_NOT_FOUND, HttpStatus.NOT_FOUND);
    }
    if (membership.isOwner) {
      throw new AppException(ErrorCode.STAFF_CANNOT_EDIT_OWNER, HttpStatus.FORBIDDEN);
    }
    if (membership.userId === actorUserId) {
      throw new AppException(ErrorCode.STAFF_CANNOT_CLEAR_OWN_PASSWORD, HttpStatus.BAD_REQUEST);
    }
    if (!membership.isActive) {
      throw new AppException(ErrorCode.STAFF_PASSWORD_CLEAR_ACTIVE_ONLY, HttpStatus.BAD_REQUEST);
    }

    const plainToken = this.generateInviteToken();
    const tokenHash = this.hashInviteToken(plainToken);

    const invitation = await this.prisma.$transaction(async (tx) => {
      await tx.user.update({
        where: { id: membership.userId },
        data: { passwordHash: null },
      });
      await tx.session.deleteMany({
        where: { userId: membership.userId },
      });
      await tx.staffInvitation.updateMany({
        where: {
          membershipId: membership.id,
          acceptedAt: null,
          revokedAt: null,
        },
        data: { revokedAt: new Date() },
      });
      return tx.staffInvitation.create({
        data: {
          membershipId: membership.id,
          invitedById: actorUserId,
          tokenHash,
          expiresAt: this.getInviteExpiryDate(),
        },
      });
    });

    return {
      success: true,
      data: {
        membershipId: membership.id,
        invitationId: invitation.id,
        email: membership.user.email,
        invitationUrl: this.buildInviteUrl(plainToken),
      },
    };
  }

  async acceptInvite(dto: AcceptStaffInviteDto) {
    const invitation = await this.findValidInvitation(dto.token);

    if (invitation.acceptedAt) {
      throw new AppException(ErrorCode.STAFF_INVITE_ALREADY_ACCEPTED, HttpStatus.BAD_REQUEST);
    }

    const passwordHash = await bcrypt.hash(dto.password, 10);
    const now = new Date();

    await this.prisma.$transaction(async (tx) => {
      await tx.user.update({
        where: { id: invitation.membership.userId },
        data: {
          passwordHash,
          name: dto.name.trim(),
        },
      });

      await tx.membership.update({
        where: { id: invitation.membershipId },
        data: { isActive: true },
      });

      await tx.staffInvitation.update({
        where: { id: invitation.id },
        data: { acceptedAt: now },
      });
    });

    return {
      success: true,
      data: {
        email: invitation.membership.user.email,
      },
      message: 'Invitation accepted. You can now log in.',
    };
  }

  async updateMember(
    actorUserId: string,
    organizationId: string,
    membershipId: string,
    dto: UpdateStaffMemberDto,
  ) {
    const actor = await this.getActorMembership(actorUserId, organizationId);
    if (!actor || !this.canEditStaff(actor)) {
      throw new AppException(ErrorCode.PERMISSION_EDIT_STAFF, HttpStatus.FORBIDDEN);
    }

    const target = await this.prisma.membership.findFirst({
      where: { id: membershipId, organizationId },
      include: {
        user: true,
        permissions: { include: { permission: true } },
      },
    });

    if (!target) {
      throw new AppException(ErrorCode.STAFF_MEMBER_NOT_FOUND, HttpStatus.NOT_FOUND);
    }
    if (target.isOwner) {
      throw new AppException(ErrorCode.STAFF_CANNOT_EDIT_OWNER, HttpStatus.FORBIDDEN);
    }

    if (dto.name !== undefined) {
      await this.prisma.user.update({
        where: { id: target.userId },
        data: { name: dto.name.trim() },
      });
    }

    if (dto.permissionNames !== undefined) {
      const orgType = await getOrganizationTypeName(this.prisma, organizationId);
      const normalizedPerms = filterPermissionsForOrgType(dto.permissionNames, orgType);
      const permissionRows = await this.prisma.permission.findMany({
        where: { name: { in: normalizedPerms } },
        select: { id: true, name: true },
      });
      if (permissionRows.length !== normalizedPerms.length) {
        const ok = new Set(permissionRows.map((p) => p.name));
        const missing = normalizedPerms.filter((n) => !ok.has(n));
        throw new AppException(ErrorCode.STAFF_UNKNOWN_PERMISSIONS, HttpStatus.BAD_REQUEST);
      }

      await this.prisma.$transaction([
        this.prisma.membershipPermission.deleteMany({ where: { membershipId: target.id } }),
        ...(permissionRows.length
          ? [
              this.prisma.membershipPermission.createMany({
                data: permissionRows.map((p) => ({
                  membershipId: target.id,
                  permissionId: p.id,
                })),
              }),
            ]
          : []),
      ]);
    }

    return { success: true, message: 'Member updated' };
  }

  async enableMember(actorUserId: string, organizationId: string, membershipId: string) {
    const actor = await this.getActorMembership(actorUserId, organizationId);
    if (!actor || !this.canEditStaff(actor)) {
      throw new AppException(ErrorCode.PERMISSION_EDIT_STAFF, HttpStatus.FORBIDDEN);
    }

    const target = await this.prisma.membership.findFirst({
      where: { id: membershipId, organizationId },
      include: {
        invitations: { orderBy: { createdAt: 'desc' }, take: 1 },
      },
    });

    if (!target) {
      throw new AppException(ErrorCode.STAFF_MEMBER_NOT_FOUND, HttpStatus.NOT_FOUND);
    }
    if (target.isOwner) {
      throw new AppException(ErrorCode.STAFF_CANNOT_ENABLE_OWNER, HttpStatus.FORBIDDEN);
    }
    if (target.isActive) {
      throw new AppException(ErrorCode.STAFF_ALREADY_ACTIVE, HttpStatus.BAD_REQUEST);
    }

    const invitation = target.invitations[0];
    if (invitation && !invitation.acceptedAt) {
      throw new AppException(ErrorCode.STAFF_INVITE_MISSING, HttpStatus.BAD_REQUEST);
    }

    await this.prisma.$transaction(async (tx) => {
      await this.assertOrganizationHasAvailableSeat(organizationId, tx);
      await tx.membership.update({
        where: { id: membershipId },
        data: { isActive: true },
      });
    });

    return {
      success: true,
      message: 'Member enabled. They can sign in to this organization again.',
    };
  }

  async disableMember(actorUserId: string, organizationId: string, membershipId: string) {
    const actor = await this.getActorMembership(actorUserId, organizationId);
    if (!actor || !this.canEditStaff(actor)) {
      throw new AppException(ErrorCode.PERMISSION_EDIT_STAFF, HttpStatus.FORBIDDEN);
    }

    const target = await this.prisma.membership.findFirst({
      where: { id: membershipId, organizationId },
    });

    if (!target) {
      throw new AppException(ErrorCode.STAFF_MEMBER_NOT_FOUND, HttpStatus.NOT_FOUND);
    }
    if (target.isOwner) {
      throw new AppException(ErrorCode.STAFF_CANNOT_DISABLE_OWNER, HttpStatus.FORBIDDEN);
    }
    if (actorUserId === target.userId) {
      throw new AppException(ErrorCode.STAFF_CANNOT_DISABLE_SELF, HttpStatus.BAD_REQUEST);
    }
    if (!target.isActive) {
      throw new AppException(ErrorCode.STAFF_ALREADY_DISABLED, HttpStatus.BAD_REQUEST);
    }

    await this.prisma.membership.update({
      where: { id: membershipId },
      data: { isActive: false },
    });

    await this.prisma.session.deleteMany({
      where: { userId: target.userId },
    });

    return {
      success: true,
      message: 'Member disabled. Their seat is now available for another invite.',
    };
  }

  async removeMember(actorUserId: string, organizationId: string, membershipId: string) {
    const actor = await this.getActorMembership(actorUserId, organizationId);
    if (!actor || !this.canEditStaff(actor)) {
      throw new AppException(ErrorCode.PERMISSION_EDIT_STAFF, HttpStatus.FORBIDDEN);
    }

    const target = await this.prisma.membership.findFirst({
      where: { id: membershipId, organizationId },
    });

    if (!target) {
      throw new AppException(ErrorCode.STAFF_MEMBER_NOT_FOUND, HttpStatus.NOT_FOUND);
    }
    if (target.isOwner) {
      throw new AppException(ErrorCode.STAFF_CANNOT_REMOVE_OWNER, HttpStatus.FORBIDDEN);
    }

    await this.prisma.membership.delete({ where: { id: membershipId } });

    return { success: true, message: 'Member removed' };
  }

  private async assertOrganizationHasAvailableSeat(
    organizationId: string,
    db: Prisma.TransactionClient | PrismaService = this.prisma,
  ) {
    const org = await db.organization.findUnique({
      where: { id: organizationId },
      include: { plan: true },
    });
    if (!org) {
      throw new AppException(ErrorCode.PERMISSION_ORG_NOT_FOUND, HttpStatus.NOT_FOUND);
    }
    if (!org.plan) {
      throw new AppException(ErrorCode.STAFF_NO_SUBSCRIPTION, HttpStatus.BAD_REQUEST);
    }

    const maxUsers = org.plan.maxUsers;
    const seatsUsed = await db.membership.count({
      where: {
        organizationId,
        OR: [{ isOwner: true }, { isActive: true }],
      },
    });
    if (!isUnlimitedSeats(maxUsers) && seatsUsed >= maxUsers) {
      throw new AppException(ErrorCode.STAFF_SEAT_LIMIT, HttpStatus.BAD_REQUEST);
    }
  }

  private async getActorMembership(userId: string, organizationId: string) {
    return this.prisma.membership.findFirst({
      where: { userId, organizationId },
      include: {
        permissions: { include: { permission: true } },
        organization: { select: { planId: true } },
      },
    });
  }

  private getInvitationStatus(m: {
    isOwner: boolean;
    isActive: boolean;
    invitations: { acceptedAt: Date | null; revokedAt: Date | null; expiresAt: Date }[];
  }): 'ACTIVE' | 'PENDING' | 'EXPIRED' | 'DISABLED' {
    if (m.isOwner) return 'ACTIVE';
    if (m.isActive) return 'ACTIVE';
    const invitation = m.invitations[0];
    if (invitation?.acceptedAt) return 'DISABLED';
    if (!invitation) return 'DISABLED';
    if (invitation.revokedAt) return 'EXPIRED';
    return invitation.expiresAt.getTime() > Date.now() ? 'PENDING' : 'EXPIRED';
  }

  private generateInviteToken(): string {
    return randomBytes(32).toString('hex');
  }

  private hashInviteToken(token: string): string {
    return createHash('sha256').update(token).digest('hex');
  }

  private getInviteExpiryDate(): Date {
    const d = new Date();
    d.setDate(d.getDate() + 7);
    return d;
  }

  private buildInviteUrl(token: string): string {
    const appUrl = process.env.FRONTEND_URL || 'http://localhost:3001';
    return `${appUrl}/accept-invite?token=${encodeURIComponent(token)}`;
  }

  private async findValidInvitation(token: string) {
    const invitation = await this.prisma.staffInvitation.findUnique({
      where: { tokenHash: this.hashInviteToken(token) },
      include: {
        membership: {
          include: {
            user: { select: { id: true, email: true, name: true } },
            organization: { select: { id: true, name: true } },
          },
        },
      },
    });

    if (!invitation) {
      throw new AppException(ErrorCode.STAFF_INVITE_MISSING, HttpStatus.NOT_FOUND);
    }
    if (invitation.revokedAt) {
      throw new AppException(ErrorCode.STAFF_INVITE_REVOKED, HttpStatus.BAD_REQUEST);
    }
    if (invitation.expiresAt.getTime() <= Date.now()) {
      throw new AppException(ErrorCode.STAFF_INVITE_EXPIRED, HttpStatus.BAD_REQUEST);
    }
    return invitation;
  }

  private canViewStaff(m: {
    isOwner: boolean;
    organization?: { planId: string | null };
    permissions: { permission: { name: string } }[];
  }): boolean {
    if (m.isOwner) return true;
    return m.permissions.some(
      (p) =>
        p.permission.name === 'TAB_STAFF_READ' || p.permission.name === 'TAB_STAFF_EDIT',
    );
  }

  private canEditStaff(m: {
    isOwner: boolean;
    organization?: { planId: string | null };
    permissions: { permission: { name: string } }[];
  }): boolean {
    if (m.isOwner) return Boolean(m.organization?.planId);
    return m.permissions.some((p) => p.permission.name === 'TAB_STAFF_EDIT');
  }
}
