import { Request } from 'express';
import crypto from 'crypto';
import jwt from 'jsonwebtoken';
import { google } from 'googleapis';
import { env } from '../config/env';
import {
  AuthOAuthProvider,
  isAuthOAuthProvider,
  isGoogleAuthEnabled,
  isLinkedInAuthEnabled,
  authOAuthConfig,
} from '../config/authOAuth';
import { resolveAuthOAuthRedirectUri } from '../utils/oauthRedirectUri';
import { UserRepository } from '../models/repositories/UserRepository';
import { AuditLogRepository } from '../models/repositories/AuditLogRepository';
import { AuthService } from './AuthService';
import { generateUuid } from '../utils/uuid';
import { AppError } from '../utils/AppError';
import { User } from '../models/entities/User.entity';

interface OAuthProfile {
  provider: AuthOAuthProvider;
  subject: string;
  email: string;
  fullName: string;
  avatarUrl?: string;
}

interface OAuthStatePayload {
  purpose: 'auth_oauth';
  provider: AuthOAuthProvider;
  redirectUri: string;
}

export class AuthOAuthService {
  constructor(
    private userRepo = new UserRepository(),
    private auditRepo = new AuditLogRepository(),
    private authService = new AuthService(),
  ) {}

  getAuthorizeUrl(provider: string, req?: Request): string {
    if (!isAuthOAuthProvider(provider)) {
      throw new AppError('validation_error', 400, 'Unsupported OAuth provider.');
    }

    const redirectUri = resolveAuthOAuthRedirectUri(provider, req);
    const state = jwt.sign(
      { purpose: 'auth_oauth', provider, redirectUri } satisfies OAuthStatePayload,
      env.JWT_SECRET,
      { expiresIn: 600 },
    );

    if (provider === 'google') {
      if (!isGoogleAuthEnabled()) {
        throw new AppError('service_unavailable', 503, 'Google sign-in is not configured.');
      }
      const oauth2 = new google.auth.OAuth2(
        authOAuthConfig.google.clientId,
        authOAuthConfig.google.clientSecret,
        redirectUri,
      );
      return oauth2.generateAuthUrl({
        access_type: 'online',
        prompt: 'select_account',
        scope: authOAuthConfig.google.scopes,
        state,
      });
    }

    if (!isLinkedInAuthEnabled()) {
      throw new AppError('service_unavailable', 503, 'LinkedIn sign-in is not configured.');
    }
    const params = new URLSearchParams({
      response_type: 'code',
      client_id: authOAuthConfig.linkedin.clientId,
      redirect_uri: redirectUri,
      scope: authOAuthConfig.linkedin.scopes.join(' '),
      state,
    });
    return `https://www.linkedin.com/oauth/v2/authorization?${params}`;
  }

  async handleCallback(
    provider: string,
    code: string,
    state: string,
  ): Promise<{ accessToken: string; user: User }> {
    if (!isAuthOAuthProvider(provider)) {
      throw new AppError('validation_error', 400, 'Unsupported OAuth provider.');
    }

    let statePayload: OAuthStatePayload;
    try {
      statePayload = jwt.verify(state, env.JWT_SECRET) as OAuthStatePayload;
    } catch {
      throw new AppError('validation_error', 400, 'Invalid or expired OAuth state.');
    }
    if (statePayload.purpose !== 'auth_oauth' || statePayload.provider !== provider) {
      throw new AppError('validation_error', 400, 'OAuth state mismatch.');
    }
    if (!statePayload.redirectUri) {
      throw new AppError('validation_error', 400, 'OAuth state missing redirect URI.');
    }

    const profile =
      provider === 'google'
        ? await this.fetchGoogleProfile(code, statePayload.redirectUri)
        : await this.fetchLinkedInProfile(code, statePayload.redirectUri);

    const user = await this.findOrCreateOAuthUser(profile);
    const accessToken = this.authService.issueAccessToken(user);

    await this.auditRepo.log({
      userId: user.id,
      action: `user.login.oauth.${provider}`,
      entityType: 'user',
      entityId: user.id,
    });

    return { accessToken, user };
  }

  private async fetchGoogleProfile(code: string, redirectUri: string): Promise<OAuthProfile> {
    const oauth2 = new google.auth.OAuth2(
      authOAuthConfig.google.clientId,
      authOAuthConfig.google.clientSecret,
      redirectUri,
    );
    const { tokens } = await oauth2.getToken(code);
    oauth2.setCredentials(tokens);
    const oauth2api = google.oauth2({ version: 'v2', auth: oauth2 });
    const userInfo = await oauth2api.userinfo.get();
    const email = userInfo.data.email;
    const subject = userInfo.data.id;
    if (!email || !subject) {
      throw new AppError('validation_error', 400, 'Google did not return required profile data.');
    }
    return {
      provider: 'google',
      subject,
      email,
      fullName: userInfo.data.name ?? email.split('@')[0],
      avatarUrl: userInfo.data.picture ?? undefined,
    };
  }

  private async fetchLinkedInProfile(code: string, redirectUri: string): Promise<OAuthProfile> {
    const tokenRes = await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        code,
        redirect_uri: redirectUri,
        client_id: authOAuthConfig.linkedin.clientId,
        client_secret: authOAuthConfig.linkedin.clientSecret,
      }),
    });
    if (!tokenRes.ok) {
      throw new AppError('validation_error', 400, 'LinkedIn authorization failed.');
    }
    const tokenData = (await tokenRes.json()) as { access_token?: string };
    if (!tokenData.access_token) {
      throw new AppError('validation_error', 400, 'LinkedIn did not return an access token.');
    }

    const profileRes = await fetch('https://api.linkedin.com/v2/userinfo', {
      headers: { Authorization: `Bearer ${tokenData.access_token}` },
    });
    if (!profileRes.ok) {
      throw new AppError('validation_error', 400, 'Failed to load LinkedIn profile.');
    }
    const profile = (await profileRes.json()) as {
      sub?: string;
      email?: string;
      name?: string;
      picture?: string;
    };
    if (!profile.sub || !profile.email) {
      throw new AppError('validation_error', 400, 'LinkedIn did not return required profile data.');
    }
    return {
      provider: 'linkedin',
      subject: profile.sub,
      email: profile.email,
      fullName: profile.name ?? profile.email.split('@')[0],
      avatarUrl: profile.picture,
    };
  }

  private async findOrCreateOAuthUser(profile: OAuthProfile): Promise<User> {
    const byOAuth = await this.userRepo.findByOAuth(profile.provider, profile.subject);
    if (byOAuth) {
      return byOAuth;
    }

    const byEmail = await this.userRepo.findByEmail(profile.email);
    if (byEmail) {
      if (byEmail.oauthSubject && byEmail.oauthSubject !== profile.subject) {
        throw new AppError(
          'validation_error',
          400,
          'This email is linked to a different sign-in method.',
        );
      }
      return this.userRepo.update(byEmail.id, {
        authProvider: profile.provider,
        oauthSubject: profile.subject,
        emailVerified: true,
        avatarUrl: byEmail.avatarUrl ?? profile.avatarUrl ?? null,
      });
    }

    const username = await this.suggestUsername(profile.email);
    return this.userRepo.create({
      id: generateUuid(),
      email: profile.email,
      passwordHash: null,
      authProvider: profile.provider,
      oauthSubject: profile.subject,
      fullName: profile.fullName,
      username,
      timezone: 'UTC',
      avatarUrl: profile.avatarUrl ?? null,
      emailVerified: true,
      mustChangePassword: false,
      passwordChangedAt: null,
    });
  }

  private async suggestUsername(email: string): Promise<string> {
    let base = email
      .split('@')[0]
      .toLowerCase()
      .replace(/[^a-z0-9-]/g, '-')
      .replace(/-+/g, '-')
      .replace(/^-|-$/g, '');
    if (!base) base = 'user';

    for (let attempt = 0; attempt < 20; attempt++) {
      const suffix = attempt === 0 ? '' : `-${crypto.randomInt(1000, 9999)}`;
      const candidate = `${base.slice(0, Math.max(1, 64 - suffix.length))}${suffix}`;
      if (!(await this.userRepo.findByUsername(candidate))) {
        return candidate;
      }
    }
    return `user-${generateUuid().slice(0, 8)}`;
  }
}
