// backend/src/modules/auth/auth.service.ts
import {
  Injectable,
  HttpStatus,
  HttpException,
  InternalServerErrorException
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import type { JwtSignOptions } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import * as bcrypt from 'bcrypt';
import { PrismaService } from '../../../prisma/prisma.service';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
import { CreateOrganizationDto } from './dto/create-organization.dto';
import {
  SUPPORTED_USER_LANGUAGES,
  UpdateLanguageDto,
} from './dto/update-language.dto';
import { JwtPayload } from './interfaces/jwt-payload.interface';
import { ownerPermissionsForOrgType, type OrganizationTypeName } from '../../common/organization-type';
import { getEffectivePermissionNames, getOrgTypeFromMembership, hasActivePlan, participatesInTasks, participatesInTreatments, participationPermissionsForOrgType } from '../../common/membership-permissions';
import { assertClinicOrganization, assertLabOrganization } from '../../common/organization-type';
import { StaffWorkingHoursService } from '../staff/staff-working-hours.service';
import { UpsertWorkingHoursDto } from '../staff/dto/upsert-working-hours.dto';
import { UpdateParticipationDto } from './dto/update-participation.dto';
import { SmsService } from '../sms/sms.service';
import {
  ForgotPasswordSendCodeDto,
  ForgotPasswordVerifyDto,
} from './dto/forgot-password.dto';
import { normalizeIranMobile } from '../../common/utils/mobile.util';
import { sessionExpiresAtFromNow } from '../../common/jwt-duration';
import { AppException, ErrorCode } from '../../common/errors';
import * as crypto from 'crypto';

const FORGOT_PASSWORD_PURPOSE = 'forgot_password';
const VERIFICATION_CODE_TTL_MS = 10 * 60 * 1000;
const SEND_CODE_COOLDOWN_MS = 60 * 1000;
const PASSWORD_RESET_WINDOW_MS = 15 * 60 * 1000;

const ALL_PERMISSIONS = [
  'TAB_TODAY_READ',
  'TAB_TODAY_EDIT',
  'TAB_STAFF_READ',
  'TAB_STAFF_EDIT',
  'TAB_ORGANIZATIONS_READ',
  'TAB_ORGANIZATIONS_EDIT',
  'TAB_PATIENTS_READ',
  'TAB_PATIENTS_EDIT',
  'TAB_APPOINTMENTS_READ',
  'TAB_APPOINTMENTS_EDIT',
  'TAB_TREATMENT_READ',
  'TAB_TREATMENT_EDIT',
  'TAB_CASES_READ',
  'TAB_CASES_EDIT',
  'TAB_TASKS_READ',
  'TAB_TASKS_EDIT',
  'TAB_BILLING_READ',
  'TAB_BILLING_EDIT',
  'TAB_REPORTS_READ',
  'TAB_REPORTS_EDIT',
];

const READ_ONLY_PERMISSIONS = [
  'TAB_TODAY_READ',
  'TAB_STAFF_READ',
  'TAB_ORGANIZATIONS_READ',
  'TAB_PATIENTS_READ',
  'TAB_APPOINTMENTS_READ',
  'TAB_TREATMENT_READ',
  'TAB_BILLING_READ',
  'TAB_REPORTS_READ',
];

@Injectable()
export class AuthService {
  constructor(
    private prisma: PrismaService,
    private jwtService: JwtService,
    private configService: ConfigService,
    private smsService: SmsService,
    private staffWorkingHoursService: StaffWorkingHoursService,
  ) { }

  private accessJwtSignOptions(): JwtSignOptions {
    return {
      secret: this.configService.get<string>('jwt.secret')!,
      expiresIn: this.configService.get<string>('jwt.expiresIn')!,
    } as JwtSignOptions;
  }

  private refreshJwtSignOptions(): JwtSignOptions {
    return {
      secret: this.configService.get<string>('jwt.refreshSecret')!,
      expiresIn: this.configService.get<string>('jwt.refreshExpiresIn')!,
    } as JwtSignOptions;
  }

  private sessionExpiresAt(): Date {
    const refreshExpiresIn =
      this.configService.get<string>('jwt.refreshExpiresIn') ?? '30d';
    return sessionExpiresAtFromNow(refreshExpiresIn);
  }

  private accessTokenExpiresAt(): Date {
    const accessExpiresIn =
      this.configService.get<string>('jwt.expiresIn') ?? '15m';
    return sessionExpiresAtFromNow(accessExpiresIn);
  }

  /**
   * Validate user credentials (used by LocalStrategy)
   * @param email - User's email
   * @param password - User's password
   * @returns User object without passwordHash or null if invalid
   */
  async validateUser(email: string, password: string): Promise<any> {
    try {
      const normalizedEmail = email.trim().toLowerCase();
      const user = await this.prisma.user.findUnique({
        where: { email: normalizedEmail },
        include: {
          memberships: {
            include: {
              organization: {
                include: {
                  type: true, // Include organization type (CLINIC/LAB)
                  plan: true,
                }
              },
              permissions: {
                include: {
                  permission: true, // Include permission details
                },
              },
            },
          },
        },
      });

      if (!user) {
        return null;
      }

      // Check if user has a password (might be OAuth only, but we're not using OAuth)
      if (!user.passwordHash) {
        return null;
      }

      const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
      if (!isPasswordValid) {
        return null;
      }

      // Remove sensitive data
      const { passwordHash, ...result } = user;
      return result;
    } catch (error) {
      throw new InternalServerErrorException('Error validating user');
    }
  }

  /**
   * Login user and generate tokens
   * @param loginDto - Login credentials (email, password)
   * @param user - Validated user object from LocalStrategy
   * @returns Access token, refresh token, user info, and organizations
   */
  async login(loginDto: LoginDto, user: any) {
    try {
      // Generate access token (short-lived)
      const accessPayload: JwtPayload = {
        sub: user.id,
        email: user.email,
        type: 'access'
      };

      // Generate refresh token (long-lived)
      const refreshPayload: JwtPayload = {
        sub: user.id,
        email: user.email,
        type: 'refresh'
      };

      const [accessToken, refreshToken] = await Promise.all([
        this.jwtService.signAsync(accessPayload, this.accessJwtSignOptions()),
        this.jwtService.signAsync(refreshPayload, this.refreshJwtSignOptions()),
      ]);

      // Store session in database
      await this.prisma.session.create({
        data: {
          userId: user.id,
          token: accessToken,
          refreshToken: refreshToken,
          expiresAt: this.sessionExpiresAt(),
        },
      });

      // Transform memberships to include organization info and permissions
      const organizations = this.toActiveOrganizations(user.memberships).map(membership => ({
        id: membership.organization.id,
        name: membership.organization.name,
        type: membership.organization.type.name, // 'CLINIC' or 'LAB'
        isOwner: membership.isOwner,
        permissions: this.getMembershipPermissions(membership),
        plan: membership.organization.plan
          ? {
              name: membership.organization.plan.name,
              maxUsers: membership.organization.plan.maxUsers,
              price: membership.organization.plan.price,
            }
          : undefined,
      }));

      return {
        success: true,
        data: {
          accessToken,
          refreshToken,
          accessTokenExpiresAt: this.accessTokenExpiresAt().toISOString(),
          user: this.toPublicUser(user),
          organizations,
        },
      };
    } catch (error) {
      //throw new InternalServerErrorException('Login failed');
      console.error('🔥 LOGIN ERROR FULL:', error);
      throw error;
    }
  }

  /**
   * Register a new user
   * @param registerDto - Registration data (email, password, name)
   * @returns Created user info without password
   */
  async register(registerDto: RegisterDto) {
    const { password, name, organizationName, organizationEmail, organizationType } = registerDto;
    const email = registerDto.email.trim().toLowerCase();

    if (!RegisterDto.isValidMobile(registerDto.mobile)) {
      throw new AppException(ErrorCode.AUTH_REGISTRATION_MOBILE_INVALID, HttpStatus.BAD_REQUEST);
    }

    const mobile = normalizeIranMobile(registerDto.mobile);

    // 1. Check existing user
    const existingUser = await this.prisma.user.findFirst({
      where: {
        OR: [{ email }, { mobile }],
      },
    });

    if (existingUser) {
      if (existingUser.email === email) {
        throw new AppException(ErrorCode.AUTH_REGISTRATION_EMAIL_EXISTS, HttpStatus.CONFLICT);
      }
      throw new AppException(ErrorCode.AUTH_REGISTRATION_MOBILE_EXISTS, HttpStatus.CONFLICT);
    }

    // 2. Hash password
    const hashedPassword = await bcrypt.hash(password, 10);

    // 3. Transaction (IMPORTANT)
    const result = await this.prisma.$transaction(async (tx) => {
      // Create user
      const user = await tx.user.create({
        data: {
          email,
          mobile,
          passwordHash: hashedPassword,
          name,
          trialUsedAt: new Date(),
        },
      });

      // Create organization
      const organization = await tx.organization.create({
        data: {
          name: organizationName,
          email: organizationEmail,

          owner: {
            connect: { id: user.id },
          },

          plan: {
            connect: { name: 'trial' }, // make sure this exists in DB
          },

          type: {
            connect: {
              name: organizationType, // 'CLINIC' | 'LAB'
            },
          },
        },
      });

      // Create membership (owner)
      await tx.membership.create({
        data: {
          userId: user.id,
          organizationId: organization.id,
          isOwner: true,
        },
      });

      return { user, organization };
    });

    // 4. Generate tokens (reuse login logic)
    const validatedUser = await this.validateUser(email, password);

    if (!validatedUser) {
      throw new AppException(ErrorCode.AUTH_REGISTRATION_FAILED, HttpStatus.UNAUTHORIZED);
    }

    return this.login({ email, password } as any, validatedUser);
  }

  async createOrganization(
    userId: string,
    currentOrganizationId: string | undefined,
    dto: CreateOrganizationDto,
  ) {
    const owner = await this.prisma.user.findUnique({
      where: { id: userId },
      select: { id: true },
    });

    if (!owner) {
      throw new AppException(ErrorCode.AUTH_USER_NOT_FOUND, HttpStatus.UNAUTHORIZED);
    }

    if (!currentOrganizationId) {
      throw new AppException(ErrorCode.AUTH_SELECT_ORG_FIRST, HttpStatus.FORBIDDEN);
    }

    const currentMembership = await this.prisma.membership.findUnique({
      where: {
        userId_organizationId: {
          userId,
          organizationId: currentOrganizationId,
        },
      },
      select: { isOwner: true },
    });

    if (!currentMembership?.isOwner) {
      throw new AppException(ErrorCode.AUTH_CREATE_ORG_OWNER_ONLY, HttpStatus.FORBIDDEN);
    }

    const organization = await this.prisma.$transaction(async (tx) => {
      const createdOrganization = await tx.organization.create({
        data: {
          name: dto.organizationName,
          email: dto.organizationEmail,
          owner: {
            connect: { id: userId },
          },
          type: {
            connect: { name: dto.organizationType },
          },
        },
      });

      await tx.membership.create({
        data: {
          userId,
          organizationId: createdOrganization.id,
          isOwner: true,
        },
      });
      return createdOrganization;
    });

    return {
      success: true,
      data: {
        organization: {
          id: organization.id,
          name: organization.name,
          email: organization.email,
        },
      },
    };
  }

  async getProfile(userId: string) {
    try {
      const user = await this.prisma.user.findUnique({
        where: { id: userId },
        include: {
          memberships: {
            include: {
              organization: {
                include: {
                  type: true,
                  plan: true,
                },
              },
              permissions: {
                include: {
                  permission: true,
                },
              },
            },
          },
        },
      });

      if (!user) {
        throw new AppException(ErrorCode.AUTH_USER_NOT_FOUND, HttpStatus.UNAUTHORIZED);
      }

      const { passwordHash, ...result } = user;

      // Transform memberships for frontend consumption
      const organizations = this.toActiveOrganizations(user.memberships).map(membership => ({
        id: membership.organization.id,
        name: membership.organization.name,
        type: membership.organization.type.name,
        isOwner: membership.isOwner,
        permissions: this.getMembershipPermissions(membership),
        plan: membership.organization.plan
          ? {
              name: membership.organization.plan.name,
              maxUsers: membership.organization.plan.maxUsers,
              price: membership.organization.plan.price,
            }
          : undefined,
      }));

      return {
        success: true,
        data: {
          ...result,
          organizations,
        },
      };
    } catch (error) {
      throw new InternalServerErrorException('Failed to get profile');
    }
  }

  /**
   * Logout user by invalidating their session
   * @param token - Access token to invalidate
   * @returns Success message
   */
  async logout(token: string) {
    try {
      await this.prisma.session.deleteMany({
        where: { token },
      });

      return {
        success: true,
        message: 'Logged out successfully',
      };
    } catch (error) {
      throw new InternalServerErrorException('Logout failed');
    }
  }

  /**
   * Refresh access token using refresh token
   * @param refreshToken - Valid refresh token
   * @param previousAccessToken - Expired access token used to preserve organization context
   * @returns New access token
   */
  async refreshToken(refreshToken: string, previousAccessToken?: string) {
    try {
      // Verify the refresh token
      const payload = await this.jwtService.verifyAsync(refreshToken, {
        secret: this.configService.get<string>('jwt.refreshSecret'),
      });

      // Ensure this is a refresh token
      if (payload.type !== 'refresh') {
        throw new AppException(ErrorCode.AUTH_SESSION_EXPIRED, HttpStatus.UNAUTHORIZED);
      }

      // Find session with this refresh token
      const session = await this.prisma.session.findFirst({
        where: {
          refreshToken,
          expiresAt: { gt: new Date() }
        },
        include: {
          user: {
            include: {
              memberships: {
                include: {
                  organization: {
                    include: {
                      type: true,
                      plan: true,
                    },
                  },
                  permissions: {
                    include: {
                      permission: true,
                    },
                  },
                },
              },
            },
          },
        },
      });

      if (!session) {
        throw new AppException(ErrorCode.AUTH_REFRESH_TOKEN_INVALID, HttpStatus.UNAUTHORIZED);
      }

      const organizationId = await this.resolveOrganizationIdForRefresh(
        previousAccessToken,
        session.user.memberships,
      );

      // Generate new access token
      const newAccessPayload: JwtPayload = {
        sub: session.user.id,
        email: session.user.email,
        type: 'access',
        ...(organizationId ? { organizationId } : {}),
      };

      const newAccessToken = await this.jwtService.signAsync(
        newAccessPayload,
        this.accessJwtSignOptions(),
      );

      // Update session with new access token
      await this.prisma.session.update({
        where: { id: session.id },
        data: {
          token: newAccessToken,
          expiresAt: this.sessionExpiresAt(),
        },
      });

      // Transform memberships for response
      const organizations = this.toActiveOrganizations(session.user.memberships).map(membership => ({
        id: membership.organization.id,
        name: membership.organization.name,
        type: membership.organization.type.name,
        isOwner: membership.isOwner,
        permissions: this.getMembershipPermissions(membership),
        plan: membership.organization.plan
          ? {
              name: membership.organization.plan.name,
              maxUsers: membership.organization.plan.maxUsers,
              price: membership.organization.plan.price,
            }
          : undefined,
      }));

      return {
        success: true,
        data: {
          accessToken: newAccessToken,
          accessTokenExpiresAt: this.accessTokenExpiresAt().toISOString(),
          user: this.toPublicUser(session.user),
          organizations,
        },
      };
    } catch (error) {
      if (error instanceof HttpException) {
        throw error;
      }
      if (error instanceof Error && (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError')) {
        throw new AppException(ErrorCode.AUTH_REFRESH_TOKEN_INVALID, HttpStatus.UNAUTHORIZED);
      }
      throw new AppException(ErrorCode.AUTH_REFRESH_TOKEN_INVALID, HttpStatus.UNAUTHORIZED);
    }
  }

  /**
   * Change user password
   * @param userId - User ID
   * @param oldPassword - Current password (optional when reset verified via SMS)
   * @param newPassword - New password
   * @param skipCurrentPassword - True when user verified mobile via forgot-password flow
   * @returns Success message
   */
  async changePassword(
    userId: string,
    oldPassword: string | undefined,
    newPassword: string,
    skipCurrentPassword = false,
  ) {
    try {
      const user = await this.prisma.user.findUnique({
        where: { id: userId },
      });

      if (!user || !user.passwordHash) {
        throw new AppException(ErrorCode.AUTH_USER_NOT_FOUND, HttpStatus.BAD_REQUEST);
      }

      if (skipCurrentPassword) {
        const hasRecentReset = await this.hasRecentPasswordResetVerification(userId);
        if (!hasRecentReset) {
          throw new AppException(ErrorCode.AUTH_PASSWORD_RESET_EXPIRED, HttpStatus.UNAUTHORIZED);
        }
      } else {
        if (!oldPassword) {
          throw new AppException(ErrorCode.AUTH_PASSWORD_CURRENT_REQUIRED, HttpStatus.BAD_REQUEST);
        }

        const isPasswordValid = await bcrypt.compare(oldPassword, user.passwordHash);
        if (!isPasswordValid) {
          throw new AppException(ErrorCode.AUTH_PASSWORD_INCORRECT, HttpStatus.UNAUTHORIZED);
        }
      }

      const hashedPassword = await bcrypt.hash(newPassword, 10);

      await this.prisma.user.update({
        where: { id: userId },
        data: { passwordHash: hashedPassword },
      });

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

      return {
        success: true,
        message: 'Password changed successfully. Please login again.',
      };
    } catch (error) {
      if (error instanceof HttpException) {
        throw error;
      }
      throw new InternalServerErrorException('Failed to change password');
    }
  }

  async sendForgotPasswordCode(dto: ForgotPasswordSendCodeDto) {
    if (!ForgotPasswordSendCodeDto.validateMobile(dto.mobile)) {
      throw new AppException(ErrorCode.AUTH_REGISTRATION_MOBILE_INVALID, HttpStatus.BAD_REQUEST);
    }

    const mobile = normalizeIranMobile(dto.mobile);
    const user = await this.prisma.user.findUnique({
      where: { mobile },
      select: { id: true },
    });

    if (!user) {
      return {
        success: true,
        message: 'If this mobile number is registered, a verification code has been sent.',
      };
    }

    const recentCode = await this.prisma.phoneVerificationCode.findFirst({
      where: {
        mobile,
        purpose: FORGOT_PASSWORD_PURPOSE,
        createdAt: { gt: new Date(Date.now() - SEND_CODE_COOLDOWN_MS) },
      },
      orderBy: { createdAt: 'desc' },
    });

    if (recentCode) {
      throw new AppException(ErrorCode.AUTH_VERIFICATION_RATE_LIMIT, HttpStatus.BAD_REQUEST);
    }

    const code = this.generateVerificationCode();
    const codeHash = this.hashVerificationCode(code);

    await this.prisma.phoneVerificationCode.create({
      data: {
        userId: user.id,
        mobile,
        codeHash,
        purpose: FORGOT_PASSWORD_PURPOSE,
        expiresAt: new Date(Date.now() + VERIFICATION_CODE_TTL_MS),
      },
    });

    await this.smsService.sendVerificationCode(mobile, code);

    if (this.configService.get<string>('NODE_ENV') === 'development') {
      console.log(`[dev] forgot-password code for ${mobile}: ${code}`);
    }

    return {
      success: true,
      message: 'If this mobile number is registered, a verification code has been sent.',
    };
  }

  async verifyForgotPasswordCode(dto: ForgotPasswordVerifyDto) {
    if (!ForgotPasswordSendCodeDto.validateMobile(dto.mobile)) {
      throw new AppException(ErrorCode.AUTH_REGISTRATION_MOBILE_INVALID, HttpStatus.BAD_REQUEST);
    }

    const mobile = normalizeIranMobile(dto.mobile);
    const code = dto.code.trim();

    const verification = await this.prisma.phoneVerificationCode.findFirst({
      where: {
        mobile,
        purpose: FORGOT_PASSWORD_PURPOSE,
        verifiedAt: null,
        expiresAt: { gt: new Date() },
      },
      orderBy: { createdAt: 'desc' },
    });

    if (!verification || !this.isVerificationCodeValid(code, verification.codeHash)) {
      throw new AppException(ErrorCode.AUTH_VERIFICATION_CODE_INVALID, HttpStatus.UNAUTHORIZED);
    }

    await this.prisma.phoneVerificationCode.update({
      where: { id: verification.id },
      data: { verifiedAt: new Date() },
    });

    const user = await this.prisma.user.findUnique({
      where: { mobile },
      include: {
        memberships: {
          include: {
            organization: {
              include: {
                type: true,
                plan: true,
              },
            },
            permissions: {
              include: {
                permission: true,
              },
            },
          },
        },
      },
    });

    if (!user) {
      throw new AppException(ErrorCode.AUTH_VERIFICATION_CODE_INVALID, HttpStatus.UNAUTHORIZED);
    }

    const loginResult = await this.login(
      { email: user.email, password: '' } as LoginDto,
      user,
    );

    return {
      success: true,
      data: {
        accessToken: loginResult.data.accessToken,
        refreshToken: loginResult.data.refreshToken,
        user: loginResult.data.user,
        organizations: loginResult.data.organizations,
        passwordResetVerified: true,
      },
    };
  }

  async hasRecentPasswordResetVerification(userId: string): Promise<boolean> {
    const recent = await this.prisma.phoneVerificationCode.findFirst({
      where: {
        userId,
        purpose: FORGOT_PASSWORD_PURPOSE,
        verifiedAt: { gt: new Date(Date.now() - PASSWORD_RESET_WINDOW_MS) },
      },
      orderBy: { verifiedAt: 'desc' },
    });

    return Boolean(recent);
  }

  private generateVerificationCode(): string {
    return String(Math.floor(10000 + Math.random() * 90000));
  }

  private hashVerificationCode(code: string): string {
    return crypto.createHash('sha256').update(code).digest('hex');
  }

  private isVerificationCodeValid(code: string, codeHash: string): boolean {
    return this.hashVerificationCode(code.trim()) === codeHash;
  }

  /**
   * Get all active sessions for a user
   * @param userId - User ID
   * @returns List of active sessions
   */
  async getUserSessions(userId: string) {
    try {
      const sessions = await this.prisma.session.findMany({
        where: {
          userId,
          expiresAt: { gt: new Date() },
        },
        orderBy: { createdAt: 'desc' },
      });

      return {
        success: true,
        data: sessions,
      };
    } catch (error) {
      throw new InternalServerErrorException('Failed to get sessions');
    }
  }

  /**
   * Revoke a specific session
   * @param userId - User ID
   * @param sessionId - Session ID to revoke
   * @returns Success message
   */
  async revokeSession(userId: string, sessionId: string) {
    try {
      await this.prisma.session.delete({
        where: {
          id: sessionId,
          userId, // Ensure session belongs to user
        },
      });

      return {
        success: true,
        message: 'Session revoked successfully',
      };
    } catch (error) {
      throw new InternalServerErrorException('Failed to revoke session');
    }
  }

  /**
   * Revoke all sessions for a user (except current)
   * @param userId - User ID
   * @param currentToken - Current access token to keep
   * @returns Success message
   */
  async revokeAllSessions(userId: string, currentToken: string) {
    try {
      await this.prisma.session.deleteMany({
        where: {
          userId,
          token: { not: currentToken }, // Keep current session
        },
      });

      return {
        success: true,
        message: 'All other sessions revoked successfully',
      };
    } catch (error) {
      throw new InternalServerErrorException('Failed to revoke sessions');
    }
  }

  /**
   * Validate token and return user
   * @param token - JWT token
   * @returns User info if token is valid
   */
  async validateToken(token: string) {
    try {
      const payload = await this.jwtService.verifyAsync(token, {
        secret: this.configService.get('jwt.secret'),
      });

      if (payload.type !== 'access') {
        throw new AppException(ErrorCode.AUTH_SESSION_EXPIRED, HttpStatus.UNAUTHORIZED);
      }

      const session = await this.prisma.session.findFirst({
        where: {
          token,
          expiresAt: { gt: new Date() }
        },
        include: {
          user: {
            include: {
              memberships: {
                include: {
                  organization: {
                    include: {
                      type: true,
                      plan: true,
                    },
                  },
                  permissions: {
                    include: {
                      permission: true,
                    },
                  },
                },
              },
            },
          },
        },
      });

      if (!session) {
        throw new AppException(ErrorCode.AUTH_SESSION_EXPIRED, HttpStatus.UNAUTHORIZED);
      }

      const { passwordHash, ...user } = session.user;

      const organizations = this.toActiveOrganizations(session.user.memberships).map(membership => ({
        id: membership.organization.id,
        name: membership.organization.name,
        type: membership.organization.type.name,
        isOwner: membership.isOwner,
        permissions: this.getMembershipPermissions(membership),
        plan: membership.organization.plan
          ? {
              name: membership.organization.plan.name,
              maxUsers: membership.organization.plan.maxUsers,
              price: membership.organization.plan.price,
            }
          : undefined,
      }));

      return {
        success: true,
        data: {
          user,
          organizations,
        },
      };
    } catch (error) {
      throw new AppException(ErrorCode.AUTH_SESSION_EXPIRED, HttpStatus.UNAUTHORIZED);
    }
  }

  async selectOrganization(userId: string, organizationId: string) {
    // 1. Verify membership
    const membership = await this.prisma.membership.findFirst({
      where: {
        userId,
        organizationId,
      },
      include: {
        user: true,
        organization: {
          include: {
            type: true,
            plan: true,
          },
        },
        permissions: {
          include: {
            permission: true,
          },
        },
      },
    });

    if (!membership) {
      throw new AppException(ErrorCode.AUTH_ORG_ACCESS_DENIED, HttpStatus.UNAUTHORIZED);
    }
    if (!membership.isOwner && !membership.isActive) {
      throw new AppException(ErrorCode.AUTH_INVITATION_PENDING, HttpStatus.UNAUTHORIZED);
    }

    // 2. Build payload WITH org context
    const payload = {
      sub: userId,
      email: membership.user.email,
      organizationId: membership.organizationId,
      type: 'access',
    };

    // 3. Generate new token
    const accessToken = await this.jwtService.signAsync(payload, this.accessJwtSignOptions());

    // 4. Format permissions
    const permissions = this.getMembershipPermissions(membership);

    return {
      success: true,
      data: {
        accessToken,
        organization: {
          id: membership.organization.id,
          name: membership.organization.name,
          type: membership.organization.type.name,
          isOwner: membership.isOwner,
          permissions,
          plan: membership.organization.plan
            ? {
                name: membership.organization.plan.name,
                maxUsers: membership.organization.plan.maxUsers,
                price: membership.organization.plan.price,
              }
            : undefined,
        },
      },
    };
  }

  private toActiveOrganizations(
    memberships: Array<{
      isOwner: boolean;
      isActive: boolean;
      organization: {
        id: string;
        name: string;
        type: { name: string };
        plan?: { name: string; maxUsers: number; price: number } | null;
      };
      permissions?: Array<{ permission: { name: string } }>;
    }> = [],
  ) {
    return memberships.filter((m) => m.isOwner || m.isActive);
  }

  private getMembershipPermissions(membership: {
    isOwner: boolean;
    organization: {
      plan?: { name: string; maxUsers: number; price: number } | null;
      planId?: string | null;
      type?: { name: string };
    };
    permissions?: Array<{ permission: { name: string } }>;
  }): string[] {
    return getEffectivePermissionNames(membership);
  }

  /**
   * Owner-only subscription / seat alerts for the current org (from JWT).
   * Used for a subtle warning indicator in the app shell (not staff-facing banners).
   */
  async getOwnerSubscriptionAlert(userId: string, organizationId: string | undefined) {
    if (!organizationId) {
      return {
        success: true,
        data: {
          showWarning: false,
          noActiveSubscription: false,
          seatsLow: false,
          trialEndingSoon: false,
          trialExpired: false,
          daysUntilPlanEnd: null,
          planEndsAt: null,
        },
      };
    }

    const membership = await this.prisma.membership.findFirst({
      where: { userId, organizationId },
      include: {
        organization: {
          include: { plan: true },
        },
      },
    });

    if (!membership || !membership.isOwner) {
      return {
        success: true,
        data: {
          showWarning: false,
          noActiveSubscription: false,
          seatsLow: false,
          trialEndingSoon: false,
          trialExpired: false,
          daysUntilPlanEnd: null,
          planEndsAt: null,
        },
      };
    }

    const org = membership.organization;
    const plan = org.plan;
    if (!plan) {
      return {
        success: true,
        data: {
          showWarning: true,
          noActiveSubscription: true,
          seatsLow: false,
          trialEndingSoon: false,
          trialExpired: false,
          seatsUsed: 0,
          seatsLimit: null,
          daysUntilTrialEnd: null,
          trialEndsAt: null,
          daysUntilPlanEnd: null,
          planEndsAt: null,
        },
      };
    }
    const maxUsers = plan.maxUsers;
    const seatsUsed = await this.prisma.membership.count({
      where: {
        organizationId: org.id,
        OR: [{ isOwner: true }, { isActive: true }],
      },
    });

    const unlimited = maxUsers >= 999999;
    const remaining = unlimited ? Infinity : maxUsers - seatsUsed;
    const seatsLow =
      !unlimited && remaining >= 0 && remaining <= 2 && maxUsers > 0;

    // Current pricing model: trial lasts 30 days; paid plans last 90 days.
    const durationDays = plan.name === 'trial' ? 30 : 90;
    const end = new Date(org.createdAt);
    end.setDate(end.getDate() + durationDays);
    const planEndsAt = end.toISOString();
    const ms = end.getTime() - Date.now();
    const daysUntilPlanEnd = Math.ceil(ms / (1000 * 60 * 60 * 24));

    const trialExpired = plan.name === 'trial' && daysUntilPlanEnd <= 0;
    const trialEndingSoon = plan.name === 'trial' && daysUntilPlanEnd > 0 && daysUntilPlanEnd <= 7;

    const showWarning = seatsLow || trialEndingSoon || trialExpired;

    return {
      success: true,
      data: {
        showWarning,
        noActiveSubscription: false,
        seatsLow,
        trialEndingSoon,
        trialExpired,
        seatsUsed,
        seatsLimit: maxUsers,
        daysUntilTrialEnd: plan.name === 'trial' ? daysUntilPlanEnd : null,
        trialEndsAt: plan.name === 'trial' ? planEndsAt : null,
        daysUntilPlanEnd,
        planEndsAt,
      },
    };
  }

  async updateLanguage(userId: string, dto: UpdateLanguageDto) {
    const language = dto.language;

    if (!SUPPORTED_USER_LANGUAGES.includes(language)) {
      throw new AppException(ErrorCode.VALIDATION_LANGUAGE_INVALID, HttpStatus.BAD_REQUEST);
    }

    const user = await this.prisma.user.update({
      where: { id: userId },
      data: { language },
      select: {
        id: true,
        email: true,
        name: true,
        language: true,
      },
    });

    return {
      success: true,
      data: {
        user: this.toPublicUser(user),
      },
    };
  }

  private toPublicUser(user: {
    id: string;
    email: string;
    name: string;
    language?: string | null;
    mobile?: string | null;
  }) {
    return {
      id: user.id,
      email: user.email,
      name: user.name,
      language: user.language ?? 'en',
      mobile: user.mobile ?? null,
    };
  }

  private async decodeAccessOrganizationId(token?: string): Promise<string | undefined> {
    if (!token?.trim()) {
      return undefined;
    }

    try {
      const payload = (await this.jwtService.verifyAsync(token, {
        secret: this.configService.get<string>('jwt.secret')!,
        ignoreExpiration: true,
      })) as JwtPayload;

      return typeof payload.organizationId === 'string' ? payload.organizationId : undefined;
    } catch {
      return undefined;
    }
  }

  private async resolveOrganizationIdForRefresh(
    previousAccessToken: string | undefined,
    memberships: Array<{
      organizationId: string;
      isOwner: boolean;
      isActive: boolean;
    }>,
  ): Promise<string | undefined> {
    const activeMemberships = memberships.filter((m) => m.isOwner || m.isActive);
    const candidateFromToken = await this.decodeAccessOrganizationId(previousAccessToken);

    if (candidateFromToken) {
      const stillMember = activeMemberships.some(
        (membership) => membership.organizationId === candidateFromToken,
      );
      if (stillMember) {
        return candidateFromToken;
      }
    }

    if (activeMemberships.length === 1) {
      return activeMemberships[0].organizationId;
    }

    return undefined;
  }

  private async getOwnerMembership(userId: string, organizationId: string) {
    if (!organizationId) {
      throw new AppException(ErrorCode.AUTH_ORG_NOT_SELECTED, HttpStatus.BAD_REQUEST);
    }

    const membership = await this.prisma.membership.findFirst({
      where: { userId, organizationId, isOwner: true },
      include: {
        organization: {
          include: { type: true, plan: true },
        },
        permissions: { include: { permission: true } },
      },
    });

    if (!membership) {
      throw new AppException(ErrorCode.PERMISSION_OWNER_ONLY, HttpStatus.FORBIDDEN);
    }

    return membership;
  }

  async getParticipation(userId: string, organizationId: string) {
    const membership = await this.getOwnerMembership(userId, organizationId);
    const orgType = getOrgTypeFromMembership(membership);

    const schedule = await this.prisma.staffWorkingHoursSchedule.findUnique({
      where: { membershipId: membership.id },
      include: { blocks: true },
    });

    return {
      success: true,
      data: {
        membershipId: membership.id,
        orgType,
        participatesInTreatments: participatesInTreatments(membership),
        participatesInTasks: participatesInTasks(membership),
        hasWorkingHours: (schedule?.blocks.length ?? 0) > 0,
      },
    };
  }

  async updateParticipation(
    userId: string,
    organizationId: string,
    dto: UpdateParticipationDto,
  ) {
    const membership = await this.getOwnerMembership(userId, organizationId);
    const orgType = getOrgTypeFromMembership(membership);

    if (!hasActivePlan(membership)) {
      throw new AppException(ErrorCode.PERMISSION_PARTICIPATION_SUBSCRIPTION, HttpStatus.FORBIDDEN);
    }

    if (dto.participate) {
      await this.grantOwnerParticipation(membership.id, orgType);
    } else {
      await this.revokeOwnerParticipation(membership, orgType);
    }

    const updated = await this.prisma.membership.findFirst({
      where: { id: membership.id },
      include: {
        organization: { include: { type: true, plan: true } },
        permissions: { include: { permission: true } },
      },
    });

    const schedule = await this.prisma.staffWorkingHoursSchedule.findUnique({
      where: { membershipId: membership.id },
      include: { blocks: true },
    });

    return {
      success: true,
      data: {
        membershipId: membership.id,
        orgType,
        participatesInTreatments: participatesInTreatments(updated!),
        participatesInTasks: participatesInTasks(updated!),
        hasWorkingHours: (schedule?.blocks.length ?? 0) > 0,
        permissions: getEffectivePermissionNames(updated!),
      },
    };
  }

  async getMyWorkingHours(userId: string, organizationId: string) {
    const membership = await this.getOwnerMembership(userId, organizationId);
    if (getOrgTypeFromMembership(membership) !== 'CLINIC') {
      throw new AppException(ErrorCode.PERMISSION_CLINIC_WORKING_HOURS, HttpStatus.BAD_REQUEST);
    }
    return this.staffWorkingHoursService.getMyWorkingHours(userId, organizationId);
  }

  async upsertMyWorkingHours(
    userId: string,
    organizationId: string,
    dto: UpsertWorkingHoursDto,
  ) {
    const membership = await this.getOwnerMembership(userId, organizationId);
    if (getOrgTypeFromMembership(membership) !== 'CLINIC') {
      throw new AppException(ErrorCode.PERMISSION_CLINIC_WORKING_HOURS, HttpStatus.BAD_REQUEST);
    }
    if (!participatesInTreatments(membership)) {
      throw new AppException(ErrorCode.PERMISSION_ENABLE_PARTICIPATION_FIRST, HttpStatus.FORBIDDEN);
    }
    return this.staffWorkingHoursService.upsertMyWorkingHours(userId, organizationId, dto);
  }

  private async grantOwnerParticipation(
    membershipId: string,
    orgType: OrganizationTypeName,
  ) {
    const participationNames = participationPermissionsForOrgType(orgType).filter((p) =>
      p.endsWith('_EDIT'),
    );

    const permissionRows = await this.prisma.permission.findMany({
      where: { name: { in: [...participationNames] } },
    });

    if (permissionRows.length === 0) {
      throw new InternalServerErrorException('Participation permissions are not configured');
    }

    const participationIds = (
      await this.prisma.permission.findMany({
        where: { name: { in: [...participationPermissionsForOrgType(orgType)] } },
        select: { id: true },
      })
    ).map((p) => p.id);

    await this.prisma.$transaction(async (tx) => {
      await tx.membershipPermission.deleteMany({
        where: {
          membershipId,
          permissionId: { in: participationIds },
        },
      });

      await tx.membershipPermission.createMany({
        data: permissionRows.map((p) => ({
          membershipId,
          permissionId: p.id,
        })),
        skipDuplicates: true,
      });
    });
  }

  private async revokeOwnerParticipation(
    membership: {
      id: string;
      userId: string;
      organizationId: string;
      organization: { id: string };
    },
    orgType: OrganizationTypeName,
  ) {
    if (orgType === 'CLINIC') {
      await assertClinicOrganization(this.prisma, membership.organizationId);

      const futureAppointment = await this.prisma.appointment.findFirst({
        where: {
          organizationId: membership.organizationId,
          providerUserId: membership.userId,
          startAt: { gte: new Date() },
        },
        select: { id: true },
      });

      if (futureAppointment) {
        throw new AppException(ErrorCode.CONFLICT_FUTURE_APPOINTMENTS, HttpStatus.CONFLICT);
      }
    } else {
      await assertLabOrganization(this.prisma, membership.organizationId);
    }

    const participationIds = (
      await this.prisma.permission.findMany({
        where: { name: { in: [...participationPermissionsForOrgType(orgType)] } },
        select: { id: true },
      })
    ).map((p) => p.id);

    await this.prisma.membershipPermission.deleteMany({
      where: {
        membershipId: membership.id,
        permissionId: { in: participationIds },
      },
    });
  }
}