import bcrypt from 'bcryptjs';
import jwt, { SignOptions } from 'jsonwebtoken';
import { UserRole, UserStatus } from '@prisma/client';
import { prisma } from '../../config/database';
import { env } from '../../config/env';
import { AppError } from '../../middleware/error-handler';
import { recordLoginAttempt } from '../../utils/metrics';
import { siteSettingsService } from '../settings/settings.service';
import { verificationTokenService } from '../../services/verification-token.service';
import { emailService } from '../../services/email.service';
import { getPrimaryRole } from '../../utils/roles';
import { logger } from '../../utils/logger';
import { fingerprintsMatch } from '../../utils/device-fingerprint';
import type { RegisterInput } from './auth.schemas';

interface TokenPayload {
  id: string;
  email: string;
  role: UserRole;
  roles: UserRole[];
  /** Device fingerprint (sha256 of UA + /24 IP subnet) — bound at issue time. */
  df?: string;
}

export interface TokenPair {
  accessToken: string;
  refreshToken: string;
}

type UserForToken = { id: string; email: string; role: UserRole; roles?: unknown };

/** Parse the roles JSON field into a UserRole[] array */
function parseRoles(user: UserForToken): UserRole[] {
  if (Array.isArray(user.roles) && user.roles.length > 0) {
    return user.roles as UserRole[];
  }
  return [user.role];
}

export const authService = {
  async login(email: string, password: string, fingerprint?: string) {
    const user = await prisma.user.findUnique({ where: { email } });
    if (!user) {
      recordLoginAttempt('failure');
      throw new AppError(401, 'Invalid email or password', 'INVALID_CREDENTIALS');
    }

    const valid = await bcrypt.compare(password, user.password);
    if (!valid) {
      recordLoginAttempt('failure');
      throw new AppError(401, 'Invalid email or password', 'INVALID_CREDENTIALS');
    }

    // Status-specific errors
    if (user.status === UserStatus.PENDING_VERIFICATION) {
      recordLoginAttempt('failure');
      throw new AppError(403, 'Please verify your email address before logging in', 'EMAIL_NOT_VERIFIED');
    }

    if (user.status === UserStatus.PENDING_APPROVAL) {
      recordLoginAttempt('failure');
      throw new AppError(403, 'Your account is pending admin approval', 'ACCOUNT_PENDING');
    }

    if (user.status !== UserStatus.ACTIVE) {
      recordLoginAttempt('failure');
      throw new AppError(403, `Account is ${user.status.toLowerCase()}`, 'ACCOUNT_INACTIVE');
    }

    if (user.expiresAt && user.expiresAt < new Date()) {
      recordLoginAttempt('failure');
      throw new AppError(403, 'Account has expired', 'ACCOUNT_EXPIRED');
    }

    recordLoginAttempt('success');

    await prisma.user.update({
      where: { id: user.id },
      data: { lastLoginAt: new Date() },
    });

    // Fire-and-forget: log USER_LOGIN activity on linked Contact
    prisma.contact.findFirst({
      where: { userId: user.id, mergedIntoId: null },
    }).then(async (contact) => {
      if (contact) {
        await prisma.contactActivity.create({
          data: {
            contactId: contact.id,
            type: 'USER_LOGIN',
            title: 'User logged in',
            description: `Login from ${user.email}`,
          },
        });
      }
    }).catch(err => {
      logger.warn('Login activity logging failed:', err);
    });

    const tokens = await this.generateTokenPair(user, fingerprint);
    const { password: _, ...userWithoutPassword } = user;

    return { user: userWithoutPassword, ...tokens };
  },

  async register(data: RegisterInput, fingerprint?: string) {
    // Check if public registration is enabled
    const settings = await siteSettingsService.get();
    if (!settings.enablePublicRegistration) {
      throw new AppError(403, 'Public registration is currently disabled', 'REGISTRATION_DISABLED');
    }

    const existing = await prisma.user.findUnique({ where: { email: data.email } });
    if (existing) {
      throw new AppError(409, 'Email already registered', 'EMAIL_EXISTS');
    }

    const hashedPassword = await bcrypt.hash(data.password, 12);

    // Determine if email verification is needed
    const smtpReady = await emailService.isSmtpConfigured();
    const requireVerification = settings.enableEmailVerification && smtpReady;

    const user = await prisma.user.create({
      data: {
        email: data.email,
        password: hashedPassword,
        name: data.name,
        phone: data.phone,
        role: UserRole.USER,
        roles: JSON.parse(JSON.stringify([UserRole.USER])),
        status: requireVerification ? UserStatus.PENDING_VERIFICATION : UserStatus.ACTIVE,
        emailVerified: !requireVerification,
        createdVia: 'SELF_REGISTRATION',
      },
    });

    // Fire-and-forget: process referral if invite code provided
    if (data.inviteCode) {
      import('../social/referral.service').then(({ referralService }) => {
        referralService.processRegistrationReferral(user.id, data.inviteCode).catch(err => {
          logger.warn('Referral processing failed:', err);
        });
      }).catch(() => {});
    }

    // Fire-and-forget: auto-link or create Contact if People feature is enabled
    siteSettingsService.get().then(async (s) => {
      if (!s.enablePeople) return;
      // Check for existing Contact with matching email → link
      const existingContact = await prisma.contact.findFirst({
        where: { email: { equals: data.email, mode: 'insensitive' }, userId: null, mergedIntoId: null },
      });
      if (existingContact) {
        await prisma.contact.update({ where: { id: existingContact.id }, data: { userId: user.id } });
        logger.info(`Auto-linked contact ${existingContact.id} to registered user ${user.id}`);
      } else {
        // Create new Contact linked to the user
        await prisma.contact.create({
          data: {
            displayName: data.name || data.email,
            firstName: data.name?.split(' ')[0] || null,
            lastName: data.name?.split(' ').slice(1).join(' ') || null,
            email: data.email,
            phone: data.phone || null,
            primarySource: 'USER',
            userId: user.id,
            tags: [],
          },
        });
        logger.info(`Auto-created contact for registered user ${user.id}`);
      }
    }).catch(err => {
      logger.warn('Contact auto-creation on register failed:', err);
    });

    // If verification required, send email and don't issue tokens
    if (requireVerification) {
      const token = await verificationTokenService.createToken(user.id);
      const adminUrl = env.ADMIN_URL || 'http://localhost:3000';
      const verificationUrl = `${adminUrl}/verify-email?token=${token}`;

      await emailService.sendVerificationEmail({
        recipientEmail: user.email,
        recipientName: user.name || 'there',
        verificationUrl,
      });

      const { password: _, ...userWithoutPassword } = user;
      return {
        user: userWithoutPassword,
        requiresVerification: true,
        message: 'Please check your email to verify your account',
      };
    }

    // No verification needed — issue tokens immediately
    const tokens = await this.generateTokenPair(user, fingerprint);
    const { password: _, ...userWithoutPassword } = user;

    return { user: userWithoutPassword, ...tokens };
  },

  async refreshTokens(refreshToken: string, currentFingerprint?: string) {
    let payload: TokenPayload;
    try {
      payload = jwt.verify(refreshToken, env.JWT_REFRESH_SECRET, { algorithms: ['HS256'] }) as TokenPayload;
    } catch {
      throw new AppError(401, 'Invalid refresh token', 'INVALID_REFRESH_TOKEN');
    }

    // Device-fingerprint binding (2026-04-12). Tokens issued after this change
    // include a `df` claim; reject if the refreshing client's fingerprint doesn't
    // match. Tokens issued before this change have no `df` — accept them once,
    // then rotate into a bound token below (grace period for existing sessions).
    if (payload.df && currentFingerprint && !fingerprintsMatch(payload.df, currentFingerprint)) {
      // Potential token theft — revoke all refresh tokens for this user as a
      // defense-in-depth measure, then deny.
      await prisma.refreshToken.deleteMany({ where: { userId: payload.id } });
      logger.warn('Refresh token fingerprint mismatch; all sessions revoked');
      throw new AppError(401, 'Session security check failed', 'FINGERPRINT_MISMATCH');
    }

    const stored = await prisma.refreshToken.findUnique({
      where: { token: refreshToken },
      include: { user: true },
    });

    if (!stored) {
      throw new AppError(401, 'Refresh token not found', 'INVALID_REFRESH_TOKEN');
    }

    // Check user status — banned/inactive users must not get new tokens
    if (stored.user.status !== UserStatus.ACTIVE) {
      await prisma.refreshToken.delete({ where: { id: stored.id } });
      throw new AppError(401, 'Account is not active', 'ACCOUNT_INACTIVE');
    }

    // Check account expiry
    if (stored.user.expiresAt && stored.user.expiresAt < new Date()) {
      await prisma.refreshToken.delete({ where: { id: stored.id } });
      throw new AppError(401, 'Account has expired', 'ACCOUNT_EXPIRED');
    }

    if (stored.expiresAt < new Date()) {
      await prisma.refreshToken.delete({ where: { id: stored.id } });
      throw new AppError(401, 'Refresh token expired', 'REFRESH_TOKEN_EXPIRED');
    }

    // Rotate: delete old and create new atomically
    const tokens = await prisma.$transaction(async (tx) => {
      await tx.refreshToken.delete({ where: { id: stored.id } });

      const userRoles = parseRoles(stored.user);
      const accessToken = this.generateAccessToken(stored.user);
      const refreshPayload: TokenPayload = {
        id: stored.user.id,
        email: stored.user.email,
        role: getPrimaryRole(userRoles),
        roles: userRoles,
        // Carry forward fingerprint from the client's current request so rotated
        // tokens stay bound to the device.
        df: currentFingerprint,
      };
      const refreshToken = jwt.sign(refreshPayload, env.JWT_REFRESH_SECRET, {
        algorithm: 'HS256',
        expiresIn: env.JWT_REFRESH_EXPIRY as SignOptions['expiresIn'],
      });

      const decoded = jwt.decode(refreshToken) as { exp: number };
      const expiresAt = new Date(decoded.exp * 1000);

      await tx.refreshToken.create({
        data: {
          token: refreshToken,
          userId: stored.user.id,
          expiresAt,
        },
      });

      return { accessToken, refreshToken };
    });

    const { password: _, ...userWithoutPassword } = stored.user;

    return { user: userWithoutPassword, ...tokens };
  },

  async logout(refreshToken: string) {
    await prisma.refreshToken.deleteMany({ where: { token: refreshToken } });
  },

  generateAccessToken(user: UserForToken): string {
    const userRoles = parseRoles(user);
    const payload: TokenPayload = {
      id: user.id,
      email: user.email,
      role: getPrimaryRole(userRoles),
      roles: userRoles,
    };
    return jwt.sign(payload, env.JWT_ACCESS_SECRET, {
      algorithm: 'HS256',
      expiresIn: env.JWT_ACCESS_EXPIRY as SignOptions['expiresIn'],
    });
  },

  async generateRefreshToken(user: UserForToken, fingerprint?: string): Promise<string> {
    const userRoles = parseRoles(user);
    const payload: TokenPayload = {
      id: user.id,
      email: user.email,
      role: getPrimaryRole(userRoles),
      roles: userRoles,
      df: fingerprint,
    };
    const token = jwt.sign(payload, env.JWT_REFRESH_SECRET, {
      algorithm: 'HS256',
      expiresIn: env.JWT_REFRESH_EXPIRY as SignOptions['expiresIn'],
    });

    const decoded = jwt.decode(token) as { exp: number };
    const expiresAt = new Date(decoded.exp * 1000);

    await prisma.refreshToken.create({
      data: {
        token,
        userId: user.id,
        expiresAt,
      },
    });

    return token;
  },

  async generateTokenPair(user: UserForToken, fingerprint?: string): Promise<TokenPair> {
    const accessToken = this.generateAccessToken(user);
    const refreshToken = await this.generateRefreshToken(user, fingerprint);
    return { accessToken, refreshToken };
  },
};
