import { HttpStatus, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService, type JwtSignOptions } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { createHash, randomBytes, randomUUID } from 'crypto';
import * as bcrypt from 'bcryptjs';
import { UserService, type PublicUser } from '../user/user.service';
import type { User } from '../user/entities/user.entity';
import { MailService } from '../mail/mail.service';
import { RefreshSession } from './entities/refresh-session.entity';
import { BusinessException } from '../../common/exceptions/business.exception';
import { SignupDto } from './dto/signup.dto';
import { LoginDto } from './dto/login.dto';
import type { JwtPayload } from './types/jwt-payload';

// bcrypt 해시 라운드
const BCRYPT_ROUNDS = 12;
// 이메일 인증 토큰 유효 시간 (24시간)
const VERIFY_TTL_MS = 24 * 60 * 60 * 1000;
// 비밀번호 재설정 토큰 유효 시간 (1시간 — 보안상 짧게)
const PASSWORD_RESET_TTL_MS = 60 * 60 * 1000;
// refresh 세션 만료(=refresh TTL, 정리용). JWT_REFRESH_TTL(기본 14d)과 맞춘다.
const REFRESH_SESSION_TTL_MS = 14 * 24 * 60 * 60 * 1000;
// 동시 갱신(재시도) 유예 — 직전 jti 가 이 시간 내 재제출이면 재사용으로 보지 않음
const REFRESH_GRACE_MS = 15 * 1000;

// 발급된 토큰 쌍
export interface TokenPair {
  accessToken: string;
  refreshToken: string;
}

// 회원가입 결과 — 인증 메일 발송 후 대기 상태(자동 로그인하지 않음)
export interface SignupResult {
  email: string;
  verificationPending: true;
}

// 소셜 로그인 프로필 — 각 전략(google/kakao)이 공통 형태로 정규화해 전달
export interface OAuthProfile {
  provider: string;
  email: string | null;
  // 제공자가 이메일 소유를 검증했는지 — 미검증이면 계정 매칭/생성을 거부(탈취/선점 방지)
  emailVerified: boolean;
  name: string;
  avatarUrl?: string | null;
}

// 인증 비즈니스 로직 — 가입, 로그인 검증, 이메일 인증, 소셜 로그인, 토큰 발급
@Injectable()
export class AuthService {
  constructor(
    private readonly userService: UserService,
    private readonly jwtService: JwtService,
    private readonly config: ConfigService,
    private readonly mailService: MailService,
    @InjectRepository(RefreshSession)
    private readonly refreshRepo: Repository<RefreshSession>,
  ) {}

  // 회원가입 — 인증 완료 계정만 중복 거부. 미인증(가입 대기) 계정은 재가입을 허용한다.
  // (인증 메일을 받기 전 사이트를 닫아 가입을 못 끝낸 경우, 같은 이메일로 다시 가입해
  //  새 인증 메일을 받아 가입을 완료할 수 있게 한다. 자동 로그인은 하지 않음)
  async signup(dto: SignupDto): Promise<SignupResult> {
    const existing = await this.userService.findByEmail(dto.email);
    // 이미 인증을 마친 계정(로컬·소셜)은 재가입 불가
    if (existing && (existing.emailVerified || existing.provider !== 'local')) {
      throw new BusinessException(
        'BIZ_001',
        '이미 가입된 이메일입니다.',
        HttpStatus.CONFLICT,
      );
    }
    const passwordHash = await bcrypt.hash(dto.password, BCRYPT_ROUNDS);

    // 미인증 로컬 계정이 있으면 입력값으로 갱신 후 인증 메일만 재발송(중복 계정 생성 X)
    if (existing) {
      await this.userService.updateRegistration(existing.id, {
        passwordHash,
        name: dto.name,
        role: dto.role ?? null,
      });
      existing.name = dto.name; // 메일 인사말에 최신 이름 반영
      await this.sendVerification(existing);
      return { email: existing.email, verificationPending: true };
    }

    const user = await this.userService.create({
      email: dto.email,
      passwordHash,
      name: dto.name,
      role: dto.role ?? null,
    });
    await this.sendVerification(user);
    return { email: user.email, verificationPending: true };
  }

  // 인증 메일 재발송 — 사용자 존재/상태를 노출하지 않기 위해 항상 동일하게 응답(미인증 로컬만 실제 발송)
  async resendVerification(email: string): Promise<void> {
    const user = await this.userService.findByEmail(email);
    if (user && user.provider === 'local' && !user.emailVerified) {
      await this.sendVerification(user);
    }
  }

  // 이메일 인증 — 토큰(원문) 해시로 사용자 조회 후 만료 검사하여 인증 완료 처리
  async verifyEmail(token: string): Promise<void> {
    if (!token) {
      throw new BusinessException(
        'BIZ_001',
        '잘못된 인증 링크입니다.',
        HttpStatus.BAD_REQUEST,
      );
    }
    const tokenHash = this.hashToken(token);
    const user = await this.userService.findByEmailVerifyTokenHash(tokenHash);
    if (
      !user ||
      !user.emailVerifyExpiresAt ||
      user.emailVerifyExpiresAt.getTime() < Date.now()
    ) {
      throw new BusinessException(
        'BIZ_001',
        '인증 링크가 만료되었거나 유효하지 않습니다. 인증 메일을 다시 받아 주세요.',
        HttpStatus.BAD_REQUEST,
      );
    }
    await this.userService.markEmailVerified(user.id);
  }

  // 비밀번호 재설정 요청 — 존재/상태를 노출하지 않기 위해 항상 동일하게 응답.
  // 로컬(이메일) 계정만 실제 재설정 메일 발송(소셜 전용은 비밀번호가 없음).
  async requestPasswordReset(email: string): Promise<void> {
    const user = await this.userService.findByEmail(email);
    if (!user || user.provider !== 'local') {
      return;
    }
    const rawToken = randomBytes(32).toString('hex');
    const tokenHash = this.hashToken(rawToken);
    const expiresAt = new Date(Date.now() + PASSWORD_RESET_TTL_MS);
    await this.userService.setPasswordResetToken(user.id, tokenHash, expiresAt);

    const resetUrl = `${this.webUrl()}/reset-password?token=${rawToken}`;
    this.mailService.sendPasswordResetEmail(user.email, user.name, resetUrl);
  }

  // 비밀번호 재설정 — 토큰(원문) 해시로 조회 + 만료 검사 후 새 비밀번호 저장.
  // 보안상 비밀번호 변경 시 해당 사용자의 모든 세션을 폐기(전 기기 강제 로그아웃).
  async resetPassword(token: string, newPassword: string): Promise<void> {
    if (!token) {
      throw this.invalidResetLink();
    }
    const tokenHash = this.hashToken(token);
    const user = await this.userService.findByPasswordResetTokenHash(tokenHash);
    if (
      !user ||
      !user.passwordResetExpiresAt ||
      user.passwordResetExpiresAt.getTime() < Date.now()
    ) {
      throw this.invalidResetLink();
    }
    const passwordHash = await bcrypt.hash(newPassword, BCRYPT_ROUNDS);
    await this.userService.updatePassword(user.id, passwordHash);
    await this.revokeAllSessions(user.id);
  }

  // 로그인 검증 — 자격 확인 + 소셜전용/미인증 분기
  async validateUser(dto: LoginDto): Promise<PublicUser> {
    const user = await this.userService.findByEmailWithPassword(dto.email);
    if (!user) {
      throw this.invalidCredentials();
    }
    // 비밀번호 미설정 = 소셜 전용 계정 → 소셜 로그인 안내
    if (user.passwordHash == null) {
      throw new BusinessException(
        'BIZ_001',
        '소셜(구글/카카오) 계정으로 가입된 이메일입니다. 소셜 로그인을 이용해 주세요.',
        HttpStatus.UNAUTHORIZED,
      );
    }
    if (!(await bcrypt.compare(dto.password, user.passwordHash))) {
      throw this.invalidCredentials();
    }
    // 이메일 미인증 → 가입 미완료로 로그인 차단
    if (!user.emailVerified) {
      throw new BusinessException(
        'BIZ_001',
        '이메일 인증이 완료되지 않았습니다. 받은 인증 메일의 링크를 클릭해 가입을 완료해 주세요.',
        HttpStatus.FORBIDDEN,
      );
    }
    return UserService.toPublic(user);
  }

  // 소셜 로그인 — 이메일로 기존 계정 매칭(있으면 로그인/인증보정), 없으면 신규 생성
  async validateOrCreateOAuthUser(profile: OAuthProfile): Promise<PublicUser> {
    if (!profile.email) {
      throw new BusinessException(
        'BIZ_001',
        '소셜 계정에서 이메일을 가져오지 못했습니다. 이메일 제공에 동의한 뒤 다시 시도해 주세요.',
        HttpStatus.BAD_REQUEST,
      );
    }
    // 제공자가 이메일을 검증하지 않았으면 거부 — 미검증 이메일로 타인 계정 탈취/선점 방지
    if (!profile.emailVerified) {
      throw new BusinessException(
        'BIZ_001',
        '소셜 계정의 이메일이 인증되지 않았습니다. 제공자에서 이메일 인증을 완료한 뒤 다시 시도해 주세요.',
        HttpStatus.FORBIDDEN,
      );
    }
    let user = await this.userService.findByEmail(profile.email);
    if (!user) {
      user = await this.userService.createOAuthUser({
        email: profile.email,
        name: profile.name,
        provider: profile.provider,
        avatarUrl: profile.avatarUrl ?? null,
      });
    } else if (!user.emailVerified) {
      // 기존(미인증 로컬) 계정과 이메일이 같으면 소셜 인증으로 인증 완료 처리하고 로그인
      await this.userService.markEmailVerified(user.id);
      user.emailVerified = true;
    }
    return UserService.toPublic(user);
  }

  // 로그인/소셜/가입 등 — 새 세션(패밀리)을 만들고 access/refresh 토큰 발급
  async issueTokensForNewSession(user: PublicUser): Promise<TokenPair> {
    // 만료된 이 사용자의 세션 정리(누적 방지)
    await this.refreshRepo
      .createQueryBuilder()
      .delete()
      .where('user_id = :userId', { userId: user.id })
      .andWhere('expires_at < :now', { now: new Date() })
      .execute();

    const jti = randomUUID();
    const session = await this.refreshRepo.save(
      this.refreshRepo.create({
        user: { id: user.id },
        currentJti: jti,
        prevJti: null,
        prevAt: null,
        expiresAt: new Date(Date.now() + REFRESH_SESSION_TTL_MS),
      }),
    );
    return this.signTokens(user.id, user.email, session.id, jti);
  }

  // refresh 회전 — 현재 토큰이면 새 jti 로 회전, 직전 토큰이면 유예 내 양성으로 처리,
  // 그 외(이미 회전된/위조 토큰 재사용)면 탈취로 간주해 패밀리 폐기 후 401.
  async rotateTokens(refreshToken: string | undefined): Promise<TokenPair> {
    if (!refreshToken) throw new UnauthorizedException();
    let payload: JwtPayload;
    try {
      payload = await this.jwtService.verifyAsync<JwtPayload>(refreshToken, {
        secret: this.config.getOrThrow<string>('JWT_REFRESH_SECRET'),
      });
    } catch {
      throw new UnauthorizedException();
    }
    if (!payload.sub || !payload.fid || !payload.jti) {
      throw new UnauthorizedException();
    }

    const session = await this.refreshRepo.findOne({
      where: { id: payload.fid },
      relations: ['user'],
    });
    if (!session || session.user?.id !== payload.sub) {
      throw new UnauthorizedException();
    }

    // 1) 정상 회전 — 현재 jti 일치 → 새 jti 발급, 직전 jti 기록
    if (payload.jti === session.currentJti) {
      const newJti = randomUUID();
      session.prevJti = session.currentJti;
      session.prevAt = new Date();
      session.currentJti = newJti;
      session.expiresAt = new Date(Date.now() + REFRESH_SESSION_TTL_MS);
      await this.refreshRepo.save(session);
      return this.signTokens(
        session.user.id,
        session.user.email,
        session.id,
        newJti,
      );
    }

    // 2) 동시 갱신(재시도) 유예 — 직전 jti 가 유예 시간 내면 양성으로 보고 현재 토큰 재발급(추가 회전 X)
    if (
      payload.jti === session.prevJti &&
      session.prevAt &&
      Date.now() - session.prevAt.getTime() < REFRESH_GRACE_MS
    ) {
      return this.signTokens(
        session.user.id,
        session.user.email,
        session.id,
        session.currentJti,
      );
    }

    // 3) 그 외 = 재사용 탐지 → 패밀리 전체 폐기(공격자·피해자 모두 강제 로그아웃)
    await this.refreshRepo.delete({ id: session.id });
    throw new UnauthorizedException();
  }

  // 로그아웃 — refresh 토큰의 세션(패밀리)만 폐기. 무효 토큰이면 조용히 무시.
  async revokeSession(refreshToken: string | undefined): Promise<void> {
    if (!refreshToken) return;
    try {
      const payload = await this.jwtService.verifyAsync<JwtPayload>(
        refreshToken,
        { secret: this.config.getOrThrow<string>('JWT_REFRESH_SECRET') },
      );
      if (payload?.fid) {
        await this.refreshRepo.delete({ id: payload.fid });
      }
    } catch {
      // 무효/만료 refresh 토큰 — 무시
    }
  }

  // 사용자의 모든 세션 폐기 — 비밀번호 변경/계정 보안 시 호출(전 기기 강제 로그아웃). 현재 미사용.
  async revokeAllSessions(userId: string): Promise<void> {
    await this.refreshRepo
      .createQueryBuilder()
      .delete()
      .where('user_id = :userId', { userId })
      .execute();
  }

  // access/refresh 토큰 서명 — refresh 에는 세션 fid + 현재 jti 를 담는다
  private async signTokens(
    sub: string,
    email: string | undefined,
    fid: string,
    jti: string,
  ): Promise<TokenPair> {
    const accessToken = await this.jwtService.signAsync(
      { sub, email },
      {
        secret: this.config.getOrThrow<string>('JWT_ACCESS_SECRET'),
        expiresIn: (this.config.get<string>('JWT_ACCESS_TTL') ??
          '15m') as JwtSignOptions['expiresIn'],
      },
    );
    const refreshToken = await this.jwtService.signAsync(
      { sub, fid, jti },
      {
        secret: this.config.getOrThrow<string>('JWT_REFRESH_SECRET'),
        expiresIn: (this.config.get<string>('JWT_REFRESH_TTL') ??
          '14d') as JwtSignOptions['expiresIn'],
      },
    );
    return { accessToken, refreshToken };
  }

  // 인증 토큰 생성·저장 후 인증 메일(현재 로그) 발송
  private async sendVerification(user: User): Promise<void> {
    const rawToken = randomBytes(32).toString('hex');
    const tokenHash = this.hashToken(rawToken);
    const expiresAt = new Date(Date.now() + VERIFY_TTL_MS);
    await this.userService.setEmailVerifyToken(user.id, tokenHash, expiresAt);

    const apiUrl = (
      this.config.get<string>('APP_API_URL') ?? 'http://localhost:3100/api'
    ).replace(/\/$/, '');
    const verifyUrl = `${apiUrl}/auth/verify-email?token=${rawToken}`;
    this.mailService.sendVerificationEmail(user.email, user.name, verifyUrl);
  }

  // 토큰 원문 → SHA-256 해시(원문은 메일 링크에만, DB엔 해시만 저장)
  private hashToken(raw: string): string {
    return createHash('sha256').update(raw).digest('hex');
  }

  // 프론트엔드(웹) 기준 URL — 비밀번호 재설정 폼 링크 대상(컨트롤러 webUrl 과 동일 정책)
  private webUrl(): string {
    const url =
      this.config.get<string>('FRONTEND_URL') ||
      this.config.get<string>('CORS_ORIGIN') ||
      'http://localhost:5273';
    return url.split(',')[0].trim().replace(/\/$/, '');
  }

  // 재설정 링크 무효/만료 공통 예외
  private invalidResetLink(): BusinessException {
    return new BusinessException(
      'BIZ_001',
      '비밀번호 재설정 링크가 만료되었거나 유효하지 않습니다. 다시 요청해 주세요.',
      HttpStatus.BAD_REQUEST,
    );
  }

  // 자격 불일치 공통 예외(이메일/비밀번호 구분 없이 동일 문구 — 계정 존재 노출 방지)
  private invalidCredentials(): BusinessException {
    return new BusinessException(
      'BIZ_001',
      '이메일 또는 비밀번호가 올바르지 않습니다.',
      HttpStatus.UNAUTHORIZED,
    );
  }
}
