import {
  Controller,
  Post,
  Put,
  Get,
  Body,
  HttpCode,
  HttpStatus,
  Headers,
  UnauthorizedException,
  Req,
  Res,
  Query,
  UseGuards,
} from '@nestjs/common';
import { ThrottlerGuard } from '@nestjs/throttler';
import type { Request, Response } from 'express';
import { randomBytes } from 'crypto';
import { ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service';
import {
  LoginDto,
  RegisterDto,
  RefreshTokenDto,
  ChangePasswordDto,
  DevEmailLoginDto,
} from './dto/auth.dto';
import { Public } from '../../../common/decorators/public.decorator';
import { Auditable, Sensitive } from '@core/observability/audit/decorators/auditable.decorator';
import { SsoConfigService } from './sso/sso-config.service';
import { SsoOidcClientService } from './sso/sso-oidc-client.service';
import { SsoError, SsoErrorCode } from './sso/sso-errors';
import { getClientIp } from '@common/utils/client-ip';

// SSO 4 个 cookie 名（事实源：07-api §「Cookie 约定」）
const SSO_COOKIE_NAMES = ['sso_state', 'sso_nonce', 'sso_redirect', 'sso_code_verifier'] as const;
const SSO_COOKIE_MAX_AGE_SEC = 900; // 15 分钟
const DEFAULT_POST_LOGIN_PATH = '/overview';

/** 复用 frontend/src/lib/auth-redirect.ts isSafeRelativePath 规则（同源相对路径） */
function normalizeSafeRedirect(target: string | undefined | null): string {
  if (!target) return DEFAULT_POST_LOGIN_PATH;
  const v = target.trim();
  if (!v) return DEFAULT_POST_LOGIN_PATH;
  if (!v.startsWith('/')) return DEFAULT_POST_LOGIN_PATH;
  if (v.startsWith('//')) return DEFAULT_POST_LOGIN_PATH;
  if (v.startsWith('/login')) return DEFAULT_POST_LOGIN_PATH;
  return v;
}

// 登录类端点必须限流（规则 §2.3：30s ≤ 5 次）
@Controller('auth')
@UseGuards(ThrottlerGuard)
export class AuthController {
  constructor(
    private readonly authService: AuthService,
    private readonly ssoConfig: SsoConfigService,
    private readonly ssoOidcClient: SsoOidcClientService,
    private readonly configService: ConfigService,
  ) {}

  @Public()
  @Post('register')
  @Auditable()
  @Sensitive()
  async register(@Body() registerDto: RegisterDto) {
    return this.authService.register(registerDto);
  }

  @Public()
  @Post('login')
  @Auditable()
  @HttpCode(HttpStatus.OK)
  async login(@Body() loginDto: LoginDto) {
    return this.authService.login(loginDto);
  }

  @Public()
  @Post('dev-email-login')
  @HttpCode(HttpStatus.OK)
  async devEmailLogin(@Body() body: DevEmailLoginDto) {
    return this.authService.devEmailLogin(body);
  }

  @Public()
  @Post('refresh')
  @HttpCode(HttpStatus.OK)
  async refresh(@Body() refreshTokenDto: RefreshTokenDto) {
    const token = refreshTokenDto.refreshToken;
    if (!token) {
      throw new UnauthorizedException('No refresh token provided');
    }
    return this.authService.refresh(token);
  }

  @Post('logout')
  @Auditable()
  @HttpCode(HttpStatus.OK)
  async logout(@Headers('authorization') authHeader: string) {
    if (!authHeader) {
      throw new UnauthorizedException('No token provided');
    }

    const parts = authHeader.split(' ');
    if (parts.length !== 2 || parts[0] !== 'Bearer') {
      throw new UnauthorizedException('Invalid authorization header');
    }

    const token = parts[1];
    return this.authService.logout(token);
  }

  @Put('change-password')
  @Auditable()
  @Sensitive()
  @HttpCode(HttpStatus.OK)
  async changePassword(
    @Req() req: any,
    @Body() changePasswordDto: ChangePasswordDto,
  ) {
    // Get user ID from JWT payload (set by JwtAuthGuard)
    const userId = req.user?.userId;
    if (!userId) {
      throw new UnauthorizedException('User not authenticated');
    }

    await this.authService.changePassword(userId, changePasswordDto);

    return {
      message: 'Password changed successfully',
    };
  }

  // ===========================================================================
  // SSO 端点（v2.4 Entra ID OIDC 授权码流程 + PKCE）
  // 事实源：docs/modules/organization/07-api.md §「认证接口」第 6-7 个端点
  // ===========================================================================

  /**
   * GET /api/v1/auth/sso/start
   *
   * 生成 state / nonce / code_verifier，写 4 个 HttpOnly cookie，
   * 302 跳到 Microsoft authorize endpoint。
   */
  @Public()
  @Get('sso/start')
  async ssoStart(
    @Query('redirect') redirect: string | undefined,
    @Res() res: Response,
  ): Promise<void> {
    try {
      // 1. 生成 state / nonce / code_verifier（crypto.randomBytes(32) → base64url）
      const state = randomBytes(32).toString('base64url');
      const nonce = randomBytes(32).toString('base64url');
      // PKCE code_verifier 43-128 字符；32B base64url = 43 字符，落点合规
      const codeVerifier = randomBytes(32).toString('base64url');
      const safeRedirect = normalizeSafeRedirect(redirect);

      // 2. 写 4 个 cookie（HttpOnly + Secure(prod) + SameSite=Lax + Max-Age=900s）
      this.setSsoCookie(res, 'sso_state', state);
      this.setSsoCookie(res, 'sso_nonce', nonce);
      this.setSsoCookie(res, 'sso_redirect', safeRedirect);
      this.setSsoCookie(res, 'sso_code_verifier', codeVerifier);

      // 3. 构造 Microsoft authorize URL（含 code_challenge / code_challenge_method=S256）
      const authorizeUrl = await this.ssoOidcClient.buildAuthorizationUrl({
        state,
        nonce,
        codeVerifier,
      });

      // 4. 302 跳走
      res.redirect(302, authorizeUrl);
    } catch (err: any) {
      const code: SsoErrorCode =
        err instanceof SsoError ? err.code : 'SSO_PROVIDER_UNAVAILABLE';
      this.clearSsoCookies(res);
      res.redirect(302, `/login?ssoError=${code}`);
    }
  }

  /**
   * GET /api/v1/auth/sso/callback
   *
   * Microsoft 跳回，按 07-api §「处理流程 0-9 步」走。
   *
   * 错误统一：所有错误（SSO_*、IAM_USER_SUSPENDED）→ 302 到 /login?ssoError=<CODE>。
   * 成功：302 到 ${sso_redirect}#accessToken=<jwt>&refreshToken=<jwt>。
   */
  @Public()
  @Get('sso/callback')
  @Auditable()
  async ssoCallback(
    @Query('code') code: string | undefined,
    @Query('state') state: string | undefined,
    @Query('error') errorParam: string | undefined,
    @Query('error_description') _errorDesc: string | undefined,
    @Req() req: Request,
    @Res() res: Response,
  ): Promise<void> {
    const cookies = (req as any).cookies || {};
    const ssoState = cookies.sso_state as string | undefined;
    const ssoNonce = cookies.sso_nonce as string | undefined;
    const ssoRedirectRaw = cookies.sso_redirect as string | undefined;
    const ssoCodeVerifier = cookies.sso_code_verifier as string | undefined;
    const ssoRedirect = normalizeSafeRedirect(ssoRedirectRaw);

    const ipAddress = getClientIp(req);
    const userAgent = (req.headers['user-agent'] || 'unknown').toString();

    // Step 0: Entra error query 早返
    if (errorParam) {
      let errCode: SsoErrorCode;
      if (errorParam === 'access_denied') {
        errCode = 'SSO_USER_CANCELLED';
      } else if (errorParam === 'consent_required' || errorParam === 'interaction_required') {
        errCode = 'SSO_CONSENT_REQUIRED';
      } else {
        errCode = 'SSO_PROVIDER_REJECTED';
      }
      this.clearSsoCookies(res);
      return res.redirect(302, `/login?ssoError=${errCode}`);
    }

    try {
      // Step 1: state 校验
      if (!state || !ssoState || state !== ssoState) {
        throw new SsoError('SSO_TOKEN_INVALID', 'state mismatch or cookie missing');
      }
      if (!code) {
        throw new SsoError('SSO_TOKEN_INVALID', 'missing code');
      }
      if (!ssoCodeVerifier || !ssoNonce) {
        throw new SsoError('SSO_TOKEN_INVALID', 'sso_code_verifier or sso_nonce cookie missing');
      }

      // Step 2 + 3: token 换取 + ID token 校验（一次性完成）
      // 修：reverse proxy（如 Caddy）后端看到的 req.protocol = 'http'，但 authorize 阶段提交给
      // Entra 的 redirect_uri 是 'https://...'（来自 AZURE_REDIRECT_URI env），两者必须 100% 字符
      // 串一致，否则 Entra 在 token endpoint 返 AADSTS500112 reply address mismatch。用 env 的
      // origin（scheme + host）拼 callbackUrl，path+query 仍从 req 取，保证跟 authorize 阶段提交
      // 的字符串完全一致。详见 .learnings/ERRORS/ERR-20260520-001
      const expectedRedirect = new URL(this.ssoConfig.redirectUri);
      const callbackUrl = new URL(
        req.originalUrl || req.url,
        `${expectedRedirect.protocol}//${expectedRedirect.host}`,
      );
      const tokens = await this.ssoOidcClient.exchangeCodeForTokens({
        callbackUrl,
        codeVerifier: ssoCodeVerifier,
        expectedNonce: ssoNonce,
        expectedState: ssoState,
      });

      const claims = tokens.idTokenClaims;
      const oid = (claims.oid || claims.sub) as string | undefined;
      const entraTid = (claims.tid || '') as string;
      const rawEmail = (claims.email || '') as string;

      // Step 4: email 校验
      if (!rawEmail) {
        throw new SsoError('SSO_EMAIL_MISSING', 'id_token 缺 email claim');
      }
      if (!oid) {
        throw new SsoError('SSO_TOKEN_INVALID', 'id_token 缺 oid/sub claim');
      }

      // Step 5-7: 用户匹配 / JIT / 绑定回填（事务内）
      const result = await this.authService.loginViaSSO({
        email: rawEmail,
        oid,
        entraTid,
        ipAddress,
        userAgent,
      });

      // Step 8: 清 4 个 cookie
      this.clearSsoCookies(res);

      // Step 9: 成功 → 302 跳回前端，带 fragment
      const target = `${ssoRedirect}#accessToken=${encodeURIComponent(result.accessToken)}&refreshToken=${encodeURIComponent(result.refreshToken)}`;
      return res.redirect(302, target);
    } catch (err: any) {
      this.clearSsoCookies(res);

      // 状态非 ACTIVE 用 IAM_USER_SUSPENDED（复用现有错误码）
      if (err instanceof SsoError && (err.meta as any)?.iamUserSuspended) {
        return res.redirect(302, `/login?ssoError=IAM_USER_SUSPENDED`);
      }
      if (err instanceof SsoError) {
        return res.redirect(302, `/login?ssoError=${err.code}`);
      }
      // 兜底：未知异常 → 503
      return res.redirect(302, `/login?ssoError=SSO_PROVIDER_UNAVAILABLE`);
    }
  }

  // -------------------------- private helpers ------------------------------

  private setSsoCookie(res: Response, name: string, value: string): void {
    const isProd = (this.configService.get<string>('NODE_ENV') || '').toLowerCase() === 'production';
    res.cookie(name, value, {
      httpOnly: true,
      secure: isProd,
      sameSite: 'lax',
      maxAge: SSO_COOKIE_MAX_AGE_SEC * 1000,
      path: '/',
    });
  }

  private clearSsoCookies(res: Response): void {
    for (const name of SSO_COOKIE_NAMES) {
      res.cookie(name, '', {
        httpOnly: true,
        sameSite: 'lax',
        maxAge: 0,
        path: '/',
      });
    }
  }
}
