import { Request, Response } from 'express';
import { z } from 'zod';
import authentikService from '../services/authentik.service';
import tokenService from '../services/token.service';
import userService from '../services/user.service';
import memberService from '../services/member.service';
import logger from '../utils/logger';
import auditService, { AuditAction, AuditResourceType } from '../services/audit.service';
import { extractIp, extractUserAgent } from '../middleware/audit.middleware';
import { getUserRole } from '../middleware/rbac.middleware';
import pool from '../config/database';

/**
 * Extract given_name and family_name from Authentik userinfo.
 * Falls back to splitting the `name` field if individual fields are missing or identical.
 */
function extractNames(userInfo: { name?: string; given_name?: string; family_name?: string }): {
  given_name: string | undefined;
  family_name: string | undefined;
} {
  const givenName = userInfo.given_name?.trim();
  const familyName = userInfo.family_name?.trim();

  // If Authentik provides both and they differ, use them directly
  // BUT: guard against the case where given_name is actually the full name
  // (e.g. Authentik sends given_name="Matthias Hochmeister", family_name="Hochmeister")
  if (givenName && familyName && givenName !== familyName) {
    const looksLikeFullName =
      givenName.includes(' ') &&
      (givenName.endsWith(' ' + familyName) || givenName === familyName);
    if (!looksLikeFullName) {
      return { given_name: givenName, family_name: familyName };
    }
    // Fall through to split the name field
  }

  // Fall back to splitting the name field
  if (userInfo.name) {
    const parts = userInfo.name.trim().split(/\s+/);
    if (parts.length >= 2) {
      return {
        given_name: parts[0],
        family_name: parts.slice(1).join(' '),
      };
    }
    // Single word name — use as given_name only
    return {
      given_name: parts[0],
      family_name: undefined,
    };
  }

  return { given_name: givenName, family_name: familyName };
}

class AuthController {
  /**
   * Handle OAuth callback
   * POST /api/auth/callback
   */
  async handleCallback(req: Request, res: Response): Promise<void> {
    const ip        = extractIp(req);
    const userAgent = extractUserAgent(req);

    try {
      const callbackSchema = z.object({
        code: z.string().min(1),
        redirect_uri: z.string().url().optional(),
      });

      const parseResult = callbackSchema.safeParse(req.body);
      if (!parseResult.success) {
        res.status(400).json({
          success: false,
          message: 'Authorization code is required',
        });
        return;
      }

      const { code } = parseResult.data;

      logger.info('Processing OAuth callback', { hasCode: !!code });

      // Step 1: Exchange code for tokens
      const tokens = await authentikService.exchangeCodeForTokens(code);

      // Step 2: Get user info from Authentik
      const userInfo = await authentikService.getUserInfo(tokens.access_token);
      const groups = userInfo.groups ?? [];
      const dashboardGroups = groups.filter((g: string) => g.startsWith('dashboard_'));

      // Step 3: Verify ID token if present
      if (tokens.id_token) {
        try {
          await authentikService.verifyIdToken(tokens.id_token);
        } catch (error) {
          logger.error('ID token verification failed — continuing with userinfo (security event)', { error });
        }
      }

      // Step 4: Find or create user in database
      let user = await userService.findByAuthentikSub(userInfo.sub);
      let isNewUser = !user;

      // Check for a FDISK-pre-created account to claim on first Authentik login
      if (!user) {
        const { given_name: fdiskGivenName, family_name: fdiskFamilyName } = extractNames(userInfo);
        if (fdiskGivenName && fdiskFamilyName) {
          const fdiskUser = await userService.findFdiskUserByName(fdiskGivenName, fdiskFamilyName);
          if (fdiskUser) {
            user = await userService.claimFdiskUser(fdiskUser.id, userInfo.sub, userInfo.email);
            if (user) {
              isNewUser = false;
              logger.info('Claimed FDISK-pre-created user on first login', { userId: fdiskUser.id, sub: userInfo.sub });
            }
          }
        }
      }

      if (!user) {
        // User doesn't exist, create new user
        logger.info('Creating new user from Authentik', {
          sub:   userInfo.sub,
          email: userInfo.email,
        });

        const { given_name: newGivenName, family_name: newFamilyName } = extractNames(userInfo);

        user = await userService.createUser({
          email:              userInfo.email,
          authentik_sub:      userInfo.sub,
          preferred_username: userInfo.preferred_username,
          given_name:         newGivenName,
          family_name:        newFamilyName,
          name:               userInfo.name,
          profile_picture_url: userInfo.picture,
        });

        await userService.updateGroups(user.id, dashboardGroups);
        logger.info('Groups synced for user', { userId: user.id, groupCount: dashboardGroups.length });
        await memberService.ensureProfileExists(user.id);

        // Audit: first-ever login (user record creation)
        auditService.logAudit({
          user_id:       user.id,
          user_email:    user.email,
          action:        AuditAction.LOGIN,
          resource_type: AuditResourceType.USER,
          resource_id:   user.id,
          old_value:     null,
          new_value:     { event: 'first_login', email: user.email },
          ip_address:    ip,
          user_agent:    userAgent,
          metadata:      { new_account: true },
        });
      } else {
        // User exists — check active status BEFORE any mutations
        if (!user.is_active) {
          logger.warn('Inactive user attempted login', { userId: user.id });

          auditService.logAudit({
            user_id:       user.id,
            user_email:    user.email,
            action:        AuditAction.PERMISSION_DENIED,
            resource_type: AuditResourceType.USER,
            resource_id:   user.id,
            old_value:     null,
            new_value:     null,
            ip_address:    ip,
            user_agent:    userAgent,
            metadata:      { reason: 'account_inactive' },
          });

          res.status(403).json({
            success: false,
            message: 'User account is inactive',
          });
          return;
        }

        // User is active, proceed with login updates
        logger.info('Existing user logging in', {
          userId: user.id,
          email:  user.email,
        });

        await userService.updateLastLogin(user.id);
        await userService.updateGroups(user.id, dashboardGroups);
        logger.info('Groups synced for user', { userId: user.id, groupCount: dashboardGroups.length });
        await memberService.ensureProfileExists(user.id);

        const { given_name: updatedGivenName, family_name: updatedFamilyName } = extractNames(userInfo);

        // Refresh profile fields from Authentik on every login (including profile picture)
        await userService.updateUser(user.id, {
          name:                userInfo.name,
          given_name:          updatedGivenName,
          family_name:         updatedFamilyName,
          preferred_username:  userInfo.preferred_username,
          profile_picture_url: userInfo.picture || undefined,
        });

        // Audit: returning user login
        auditService.logAudit({
          user_id:       user.id,
          user_email:    user.email,
          action:        AuditAction.LOGIN,
          resource_type: AuditResourceType.USER,
          resource_id:   user.id,
          old_value:     null,
          new_value:     null,
          ip_address:    ip,
          user_agent:    userAgent,
          metadata:      {},
        });
      }

      // Extract normalised names once for use in the response
      const { given_name: resolvedGivenName, family_name: resolvedFamilyName } = extractNames(userInfo);

      // Step 5: Generate internal JWT token
      const role = await getUserRole(user.id);
      const accessToken = tokenService.generateToken({
        userId:       user.id,
        email:        user.email,
        authentikSub: user.authentik_sub,
        groups,
        role,
      });

      // Generate refresh token
      const refreshToken = tokenService.generateRefreshToken({
        userId: user.id,
        email:  user.email,
      });

      logger.info('User authenticated successfully', {
        userId: user.id,
        email:  user.email,
      });

      // Step 6: Return tokens and user info
      res.status(200).json({
        success: true,
        message: 'Authentication successful',
        data: {
          accessToken,
          refreshToken,
          isNewUser,
          user: {
            id:                user.id,
            email:             user.email,
            name:              userInfo.name              || user.name,
            preferredUsername: userInfo.preferred_username || user.preferred_username,
            givenName:         resolvedGivenName          || user.given_name,
            familyName:        resolvedFamilyName         || user.family_name,
            profilePictureUrl: user.profile_picture_url,
            isActive:          user.is_active,
            groups,
          },
        },
      });
    } catch (error) {
      logger.error('OAuth callback error', { error });

      // Audit the failed login attempt (user_id unknown at this point)
      auditService.logAudit({
        user_id:       null,
        user_email:    null,
        action:        AuditAction.PERMISSION_DENIED,
        resource_type: AuditResourceType.SYSTEM,
        resource_id:   null,
        old_value:     null,
        new_value:     null,
        ip_address:    ip,
        user_agent:    userAgent,
        metadata:      {
          reason: 'oauth_callback_error',
          error:  error instanceof Error ? error.message : 'unknown',
        },
      });

      const message =
        error instanceof Error ? error.message : 'Authentication failed';

      res.status(500).json({
        success: false,
        message,
      });
    }
  }

  /**
   * Handle logout
   * POST /api/auth/logout
   */
  async handleLogout(req: Request, res: Response): Promise<void> {
    const ip        = extractIp(req);
    const userAgent = extractUserAgent(req);

    try {
      // In a stateless JWT setup, logout is handled client-side by removing
      // the token.  We log the event for GDPR accountability.
      if (req.user) {
        logger.info('User logged out', {
          userId: req.user.id,
          email:  req.user.email,
        });

        auditService.logAudit({
          user_id:       req.user.id,
          user_email:    req.user.email,
          action:        AuditAction.LOGOUT,
          resource_type: AuditResourceType.USER,
          resource_id:   req.user.id,
          old_value:     null,
          new_value:     null,
          ip_address:    ip,
          user_agent:    userAgent,
          metadata:      {},
        });
      }

      res.status(200).json({
        success: true,
        message: 'Logout successful',
      });
    } catch (error) {
      logger.error('Logout error', { error });

      res.status(500).json({
        success: false,
        message: 'Logout failed',
      });
    }
  }

  /**
   * Handle token refresh
   * POST /api/auth/refresh
   */
  async handleRefresh(req: Request, res: Response): Promise<void> {
    try {
      const refreshSchema = z.object({
        refreshToken: z.string().min(1),
      });

      const parseResult = refreshSchema.safeParse(req.body);
      if (!parseResult.success) {
        res.status(400).json({
          success: false,
          message: 'Refresh token is required',
        });
        return;
      }

      const { refreshToken } = parseResult.data;

      // Verify refresh token
      let decoded;
      try {
        decoded = tokenService.verifyRefreshToken(refreshToken);
      } catch (error) {
        const message = error instanceof Error ? error.message : 'Invalid refresh token';
        res.status(401).json({
          success: false,
          message,
        });
        return;
      }

      // Get user from database
      const user = await userService.findById(decoded.userId);

      if (!user) {
        logger.warn('Refresh token valid but user not found', {
          userId: decoded.userId,
        });
        res.status(401).json({
          success: false,
          message: 'User not found',
        });
        return;
      }

      if (!user.is_active) {
        logger.warn('Inactive user attempted token refresh', {
          userId: user.id,
        });
        res.status(403).json({
          success: false,
          message: 'User account is inactive',
        });
        return;
      }

      // Generate new access token
      const role = await getUserRole(user.id);
      // Fetch groups from DB so refreshed tokens retain group info
      const groupsResult = await pool.query(
        'SELECT authentik_groups FROM users WHERE id = $1',
        [user.id]
      );
      const groups: string[] = groupsResult.rows[0]?.authentik_groups ?? [];
      const accessToken = tokenService.generateToken({
        userId:       user.id,
        email:        user.email,
        authentikSub: user.authentik_sub,
        groups,
        role,
      });

      logger.info('Token refreshed successfully', {
        userId: user.id,
        email:  user.email,
      });

      res.status(200).json({
        success: true,
        message: 'Token refreshed successfully',
        data: {
          accessToken,
        },
      });
    } catch (error) {
      logger.error('Token refresh error', { error });

      res.status(500).json({
        success: false,
        message: 'Token refresh failed',
      });
    }
  }
}

export default new AuthController();
