/**
 * Organization Service
 *
 * Business logic layer for multi-staff organization management.
 * Handles organization operations, team member invitations, role management,
 * and seat configuration.
 *
 * @module services/organization
 */

import { prisma } from '@/lib/db';
import { logger } from '@/lib/logger';
import { createId } from '@paralleldrive/cuid2';
import crypto from 'crypto';
import bcrypt from 'bcryptjs';
import type {
  OrgRole,
  Organization,
  OrganizationMember,
  OrganizationInvitation,
  SeatConfiguration,
  TIER_SEAT_LIMITS,
} from '@/lib/types/organization.types';
import {
  calculateAvailableSeats,
  getSeatConfiguration as calculateSeatConfig,
} from '@/lib/types/organization.types';

/**
 * Organization Service Class
 *
 * Provides methods for all organization management operations including:
 * - Creating and managing organizations
 * - Team member invitations and management
 * - Role and permission management
 * - Seat configuration and limits
 */
export class OrganizationService {
  /**
   * Create a new organization with owner as first member
   *
   * @param userId - ID of the user creating the organization
   * @param name - Organization name
   * @param tierId - Subscription tier ID (determines seat limits)
   * @returns Created organization
   *
   * @example
   * const org = await service.createOrganization('user-123', 'Acme Clinic', 'tier3');
   */
  async createOrganization(
    userId: string,
    name: string,
    tierId: string
  ): Promise<Organization> {
    // Check if organizations table exists in Prisma client
    // @ts-ignore - checking for table existence at runtime
    if (!prisma.organizations) {
      logger.warn('[OrganizationService] organizations table not available - organization feature not yet enabled');
      throw new Error('Organization feature not yet enabled');
    }

    // Check if user already owns an organization
    const existingOrg = await prisma.organizations.findUnique({
      where: { ownerId: userId },
    });

    if (existingOrg) {
      throw new Error('User already owns an organization');
    }

    // Get user details
    const user = await prisma.users.findUnique({
      where: { id: userId },
      select: { id: true, email: true, name: true, stripeCustomerId: true },
    });

    if (!user) {
      throw new Error('User not found');
    }

    const orgId = createId();

    // Create organization in Prisma
    const organization = await prisma.organizations.create({
      data: {
        id: orgId,
        name,
        ownerId: userId,
        stripeCustomerId: user.stripeCustomerId || null,
        includedSeats: 0, // Will be set when subscription is created
        purchasedSeats: 0,
      },
    });

    logger.info('Organization created', { organizationId: orgId, ownerId: userId });

    // Add owner as first member
    await prisma.organization_members.create({
      data: {
        id: createId(),
        organizationId: orgId,
        userId,
        role: 'OWNER',
        joinedAt: new Date(),
      },
    });

    logger.info('Owner added as first member', { organizationId: orgId, userId });

    // Create audit log
    await prisma.audit_logs.create({
      data: {
        userId,
        type: 'organization_created',
        action: `Organization created: ${name}`,
        success: true,
      },
    });

    return {
      id: organization.id,
      name: organization.name,
      ownerId: organization.ownerId,
      stripeCustomerId: organization.stripeCustomerId || undefined,
      includedSeats: organization.includedSeats,
      purchasedSeats: organization.purchasedSeats,
      createdAt: organization.createdAt,
      updatedAt: organization.updatedAt,
    };
  }

  /**
   * Get organization by owner ID
   *
   * @param userId - Owner user ID
   * @returns Organization with members
   */
  async getOrganizationByOwner(userId: string): Promise<Organization | null> {
    const org = await prisma.organizations.findUnique({
      where: { ownerId: userId },
      include: {
        members: {
          include: {
            user: {
              select: {
                id: true,
                email: true,
                name: true,
                lastLoginAt: true,
              },
            },
          },
          orderBy: { joinedAt: 'asc' },
        },
      },
    });

    if (!org) {
      return null;
    }

    return {
      id: org.id,
      name: org.name,
      ownerId: org.ownerId,
      stripeCustomerId: org.stripeCustomerId || undefined,
      includedSeats: org.includedSeats,
      purchasedSeats: org.purchasedSeats,
      createdAt: org.createdAt,
      updatedAt: org.updatedAt,
    };
  }

  /**
   * Get organization where user is a member
   *
   * @param userId - Member user ID
   * @returns Organization with members
   */
  async getOrganizationByMember(userId: string): Promise<Organization | null> {
    // Check if organization_members table exists in Prisma client
    // @ts-ignore - checking for table existence at runtime
    if (!prisma.organization_members) {
      logger.warn('[OrganizationService] organization_members table not available - organization feature not yet enabled');
      return null;
    }

    const membership = await prisma.organization_members.findFirst({
      where: { userId },
      include: {
        organization: {
          include: {
            members: {
              include: {
                user: {
                  select: {
                    id: true,
                    email: true,
                    name: true,
                    lastLoginAt: true,
                  },
                },
              },
              orderBy: { joinedAt: 'asc' },
            },
          },
        },
      },
    });

    if (!membership) {
      return null;
    }

    const org = membership.organization;

    return {
      id: org.id,
      name: org.name,
      ownerId: org.ownerId,
      stripeCustomerId: org.stripeCustomerId || undefined,
      includedSeats: org.includedSeats,
      purchasedSeats: org.purchasedSeats,
      createdAt: org.createdAt,
      updatedAt: org.updatedAt,
    };
  }

  /**
   * Get seat configuration for organization
   *
   * @param orgId - Organization ID
   * @returns Seat configuration with usage and limits
   */
  async getSeatConfiguration(orgId: string): Promise<SeatConfiguration> {
    const org = await prisma.organizations.findUnique({
      where: { id: orgId },
      include: {
        _count: {
          select: { members: true },
        },
        owner: {
          select: {
            subscriptions: {
              where: { status: { in: ['active', 'trialing'] } },
              take: 1,
              orderBy: { createdAt: 'desc' },
            },
          },
        },
      },
    });

    if (!org) {
      throw new Error('Organization not found');
    }

    // Get active subscription tier
    const subscription = org.owner.subscriptions[0];
    const tierId = subscription?.tierId || 'tier1';

    // Members count includes owner, so subtract 1 for used seats
    const usedSeats = Math.max(0, org._count.members - 1);

    return calculateSeatConfig(tierId, org.purchasedSeats, usedSeats);
  }

  /**
   * Check if organization can add a new seat
   *
   * @param orgId - Organization ID
   * @returns Whether a seat is available
   */
  async canAddSeat(orgId: string): Promise<boolean> {
    const config = await this.getSeatConfiguration(orgId);
    return config.availableSeats > 0;
  }

  /**
   * Invite a team member to the organization
   *
   * Creates a secure invitation with 7-day expiry.
   * Sends invitation email with unique token.
   *
   * @param orgId - Organization ID
   * @param email - Invitee email
   * @param name - Invitee name (optional)
   * @param role - Role to assign (ADMIN or STAFF)
   * @param invitedBy - User ID of person sending invitation
   * @returns Created invitation
   *
   * @example
   * const invite = await service.inviteTeamMember(
   *   'org-123',
   *   'staff@example.com',
   *   'John Doe',
   *   'STAFF',
   *   'user-456'
   * );
   */
  async inviteTeamMember(
    orgId: string,
    email: string,
    name: string | undefined,
    role: 'ADMIN' | 'STAFF',
    invitedBy: string
  ): Promise<OrganizationInvitation> {
    // Check if seats are available
    const canAdd = await this.canAddSeat(orgId);
    if (!canAdd) {
      throw new Error('No available seats. Please upgrade your plan or purchase additional seats.');
    }

    // Check if email is already a member
    const org = await prisma.organizations.findUnique({
      where: { id: orgId },
      include: {
        members: {
          include: {
            user: { select: { email: true } },
          },
        },
      },
    });

    if (!org) {
      throw new Error('Organization not found');
    }

    const existingMember = org.members.find((m) => m.user.email === email);
    if (existingMember) {
      throw new Error('User is already a member of this organization');
    }

    // Check for pending invitation
    const pendingInvite = await prisma.organization_invitations.findFirst({
      where: {
        organizationId: orgId,
        email,
        status: 'PENDING',
        expiresAt: { gt: new Date() },
      },
    });

    if (pendingInvite) {
      throw new Error('User already has a pending invitation');
    }

    // Generate secure token
    const token = crypto.randomBytes(32).toString('hex');

    // Set expiry to 7 days from now
    const expiresAt = new Date();
    expiresAt.setDate(expiresAt.getDate() + 7);

    const inviteId = createId();

    // Create invitation
    const invitation = await prisma.organization_invitations.create({
      data: {
        id: inviteId,
        organizationId: orgId,
        email,
        name: name || null,
        role,
        token,
        status: 'PENDING',
        expiresAt,
        invitedBy,
      },
    });

    logger.info('Team invitation created', {
      invitationId: inviteId,
      organizationId: orgId,
      email,
      role,
    });

    // Create audit log
    await prisma.audit_logs.create({
      data: {
        userId: invitedBy,
        type: 'team_invitation_sent',
        action: `Invited ${email} as ${role}`,
        success: true,
      },
    });

    return {
      id: invitation.id,
      email: invitation.email,
      name: invitation.name || undefined,
      role: invitation.role as OrgRole,
      status: invitation.status as any,
      expiresAt: invitation.expiresAt,
      sentAt: invitation.sentAt,
      invitedBy: invitation.invitedBy || undefined,
      token: invitation.token,
    };
  }

  /**
   * Accept an invitation and create user account
   *
   * @param token - Invitation token
   * @param password - New user password
   * @param name - User name (optional, overrides invitation name)
   * @returns Created user and membership
   */
  async acceptInvitation(
    token: string,
    password: string,
    name?: string
  ): Promise<{ user: any; member: any }> {
    // Find invitation
    const invitation = await prisma.organization_invitations.findUnique({
      where: { token },
      include: {
        organization: {
          select: { id: true, name: true, ownerId: true },
        },
      },
    });

    if (!invitation) {
      throw new Error('Invalid invitation token');
    }

    // Check if expired
    if (new Date() > invitation.expiresAt) {
      // Update status to expired
      await prisma.organization_invitations.update({
        where: { id: invitation.id },
        data: { status: 'EXPIRED' },
      });
      throw new Error('Invitation has expired');
    }

    // Check if already accepted or revoked
    if (invitation.status !== 'PENDING') {
      throw new Error(`Invitation is ${invitation.status.toLowerCase()}`);
    }

    // Check if user already exists
    let user = await prisma.users.findUnique({
      where: { email: invitation.email },
    });

    if (user) {
      // User exists, just add to organization
      const userId = user.id;

      // Check if already a member
      const existingMember = await prisma.organization_members.findFirst({
        where: {
          organizationId: invitation.organizationId,
          userId,
        },
      });

      if (existingMember) {
        throw new Error('User is already a member of this organization');
      }

      // Add as member
      const member = await prisma.organization_members.create({
        data: {
          id: createId(),
          organizationId: invitation.organizationId,
          userId,
          role: invitation.role as OrgRole,
          invitedBy: invitation.invitedBy,
          joinedAt: new Date(),
        },
      });

      // Update invitation status
      await prisma.organization_invitations.update({
        where: { id: invitation.id },
        data: { status: 'ACCEPTED' },
      });

      logger.info('Existing user added to organization', {
        userId,
        organizationId: invitation.organizationId,
        role: invitation.role,
      });

      return { user, member };
    }

    // Create new user
    const hashedPassword = await bcrypt.hash(password, 12);
    const userId = createId();
    const finalName = name || invitation.name || invitation.email.split('@')[0];
    const now = new Date();

    user = await prisma.users.create({
      data: {
        id: userId,
        email: invitation.email,
        name: finalName,
        password: hashedPassword,
        role: 'CUSTOMER', // Default role, organization role is separate
        isActive: true,
        emailVerified: now, // Auto-verify via invitation
        createdAt: now,
        updatedAt: now,
      },
    });

    logger.info('New user created via invitation', { userId, email: user.email });

    // Add as member
    const member = await prisma.organization_members.create({
      data: {
        id: createId(),
        organizationId: invitation.organizationId,
        userId,
        role: invitation.role as OrgRole,
        invitedBy: invitation.invitedBy,
        joinedAt: new Date(),
      },
    });

    // Update invitation status
    await prisma.organization_invitations.update({
      where: { id: invitation.id },
      data: { status: 'ACCEPTED' },
    });

    // Create audit log
    await prisma.audit_logs.create({
      data: {
        userId,
        type: 'invitation_accepted',
        action: `Accepted invitation to ${invitation.organization.name}`,
        success: true,
      },
    });

    return { user, member };
  }

  /**
   * Get all members of an organization
   *
   * @param orgId - Organization ID
   * @returns List of members with user info
   */
  async getMembers(orgId: string): Promise<OrganizationMember[]> {
    const members = await prisma.organization_members.findMany({
      where: { organizationId: orgId },
      include: {
        user: {
          select: {
            id: true,
            email: true,
            name: true,
            lastLoginAt: true,
          },
        },
      },
      orderBy: [{ role: 'desc' }, { joinedAt: 'asc' }],
    });

    return members.map((m) => ({
      id: m.id,
      userId: m.userId,
      email: m.user.email,
      name: m.user.name,
      role: m.role as OrgRole,
      joinedAt: m.joinedAt,
      lastActiveAt: m.user.lastLoginAt || undefined,
    }));
  }

  /**
   * Update member role
   *
   * @param memberId - Member ID
   * @param role - New role (ADMIN or STAFF)
   * @param updatedBy - User ID performing the update
   * @returns Updated member
   */
  async updateMember(
    memberId: string,
    role: 'ADMIN' | 'STAFF',
    updatedBy: string
  ): Promise<OrganizationMember> {
    // Get member
    const member = await prisma.organization_members.findUnique({
      where: { id: memberId },
      include: {
        user: { select: { id: true, email: true, name: true } },
      },
    });

    if (!member) {
      throw new Error('Member not found');
    }

    // Cannot change OWNER role
    if (member.role === 'OWNER') {
      throw new Error('Cannot change owner role');
    }

    // Update role
    const updated = await prisma.organization_members.update({
      where: { id: memberId },
      data: { role },
    });

    logger.info('Member role updated', {
      memberId,
      organizationId: member.organizationId,
      oldRole: member.role,
      newRole: role,
      updatedBy,
    });

    // Create audit log
    await prisma.audit_logs.create({
      data: {
        userId: updatedBy,
        type: 'member_role_updated',
        action: `Updated ${member.user.email} role from ${member.role} to ${role}`,
        success: true,
      },
    });

    return {
      id: updated.id,
      userId: updated.userId,
      email: member.user.email,
      name: member.user.name,
      role: updated.role as OrgRole,
      joinedAt: updated.joinedAt,
      lastActiveAt: updated.lastActiveAt || undefined,
    };
  }

  /**
   * Remove member from organization
   *
   * @param memberId - Member ID
   * @param removedBy - User ID performing the removal
   */
  async removeMember(memberId: string, removedBy: string): Promise<void> {
    // Get member
    const member = await prisma.organization_members.findUnique({
      where: { id: memberId },
      include: {
        user: { select: { id: true, email: true, name: true } },
        organization: { select: { id: true, name: true } },
      },
    });

    if (!member) {
      throw new Error('Member not found');
    }

    // Cannot remove OWNER
    if (member.role === 'OWNER') {
      throw new Error('Cannot remove organization owner');
    }

    // Delete member
    await prisma.organization_members.delete({
      where: { id: memberId },
    });

    logger.info('Member removed from organization', {
      memberId,
      userId: member.userId,
      email: member.user.email,
      organizationId: member.organizationId,
      removedBy,
    });

    // Create audit log
    await prisma.audit_logs.create({
      data: {
        userId: removedBy,
        type: 'member_removed',
        action: `Removed ${member.user.email} from ${member.organization.name}`,
        success: true,
      },
    });
  }

  /**
   * Revoke a pending invitation
   *
   * @param invitationId - Invitation ID
   * @param revokedBy - User ID performing the revocation
   */
  async revokeInvitation(invitationId: string, revokedBy: string): Promise<void> {
    const invitation = await prisma.organization_invitations.findUnique({
      where: { id: invitationId },
    });

    if (!invitation) {
      throw new Error('Invitation not found');
    }

    if (invitation.status !== 'PENDING') {
      throw new Error(`Invitation is already ${invitation.status.toLowerCase()}`);
    }

    // Update status
    await prisma.organization_invitations.update({
      where: { id: invitationId },
      data: { status: 'REVOKED' },
    });

    logger.info('Invitation revoked', {
      invitationId,
      email: invitation.email,
      organizationId: invitation.organizationId,
      revokedBy,
    });

    // Create audit log
    await prisma.audit_logs.create({
      data: {
        userId: revokedBy,
        type: 'invitation_revoked',
        action: `Revoked invitation for ${invitation.email}`,
        success: true,
      },
    });
  }

  /**
   * Get pending invitations for organization
   *
   * @param orgId - Organization ID
   * @returns List of pending invitations
   */
  async getPendingInvitations(orgId: string): Promise<OrganizationInvitation[]> {
    const invitations = await prisma.organization_invitations.findMany({
      where: {
        organizationId: orgId,
        status: 'PENDING',
        expiresAt: { gt: new Date() },
      },
      orderBy: { sentAt: 'desc' },
    });

    return invitations.map((inv) => ({
      id: inv.id,
      email: inv.email,
      name: inv.name || undefined,
      role: inv.role as OrgRole,
      status: inv.status as any,
      expiresAt: inv.expiresAt,
      sentAt: inv.sentAt,
      invitedBy: inv.invitedBy || undefined,
    }));
  }

  /**
   * Get user's organization role and allowed tabs
   *
   * @param userId - User ID
   * @returns User's role and permissions, or null if not in organization
   */
  async getUserOrganizationRole(
    userId: string
  ): Promise<{ role: OrgRole; organizationId: string; allowedTabs: string[] } | null> {
    // Check if organization_members table exists in Prisma client
    // @ts-ignore - checking for table existence at runtime
    if (!prisma.organization_members) {
      logger.warn('[OrganizationService] organization_members table not available - organization feature not yet enabled');
      return null;
    }

    const membership = await prisma.organization_members.findFirst({
      where: { userId },
      select: {
        role: true,
        organizationId: true,
      },
    });

    if (!membership) {
      return null;
    }

    const role = membership.role as OrgRole;

    // Import allowed tabs from types
    const { STAFF_ALLOWED_TABS, ADMIN_ALLOWED_TABS, OWNER_ALLOWED_TABS } =
      await import('@/lib/types/organization.types');

    let allowedTabs: string[] = [];
    switch (role) {
      case 'OWNER':
        allowedTabs = OWNER_ALLOWED_TABS;
        break;
      case 'ADMIN':
        allowedTabs = ADMIN_ALLOWED_TABS;
        break;
      case 'STAFF':
        allowedTabs = STAFF_ALLOWED_TABS;
        break;
    }

    return {
      role,
      organizationId: membership.organizationId,
      allowedTabs,
    };
  }
}

// Export singleton instance
export const organizationService = new OrganizationService();
