import type { Request, Response, NextFunction } from 'express';
import type { Prisma } from '@prisma/client';
import * as authModel from '../models/auth.model.js';
import { firebaseAuth } from '../config/firebase.js';
import { signInWithPassword, refreshIdToken, FirebaseRestError } from '../utils/firebaseIdentity.js';
import { isOrganizationSuspended } from '../utils/organizationStatus.js';
import { successResponse, errorResponse, tenantSuspendedResponse } from '../utils/response.js';
import { buildSession, findSessionUser } from '../models/session.model.js';
import type { ChangePasswordBody, UpdateProfileBody } from '../validators/auth.validator.js';
import { imageContentType, objectKey } from '../middlewares/upload.js';
import { putFile } from '../utils/storage.js';
import { removeStoredImage } from '../utils/storedImages.js';
import logger from '../config/logger.js';

// Sets both the existing header pair (kept for whatever already reads them)
// and body fields `accessToken`/`refreshToken` — a client with no Firebase
// SDK of its own (e.g. blenaxis-super-admin, a plain REST consumer) can only
// read the response body, not response headers, via its HTTP client.
function setSession(res: Response, session: { idToken: string; refreshToken: string }) {
  res.set('X-Id-Token', session.idToken);
  res.set('X-Refresh-Token', session.refreshToken);
  return { accessToken: session.idToken, refreshToken: session.refreshToken };
}

type UserWithRole = Prisma.UserGetPayload<{ include: { role: true } }>;

function toPublicUser(user: UserWithRole) {
  return {
    id: user.id,
    name: user.name,
    email: user.email,
    role: user.role?.name ?? 'user',
    avatarUrl: user.avatarUrl,
    emailVerified: user.emailVerified,
  };
}

function isFirebaseAdminError(error: unknown): error is { code: string; message: string } {
  return typeof error === 'object' && error !== null && 'code' in error;
}

// Only email + password come from the client (see auth.validator.ts). `name`
// has no signup field yet, so it's defaulted here rather than asked for.
async function signup(req: Request, res: Response, next: NextFunction) {
  try {
    const { email, password } = req.body;

    const existing = await authModel.findUserByEmail(email);
    if (existing) return void errorResponse(res, 'Email is already registered', 409);

    let firebaseUser;
    try {
      firebaseUser = await firebaseAuth.createUser({ email, password });
    } catch (error) {
      if (isFirebaseAdminError(error) && error.code === 'auth/email-already-exists') {
        return void errorResponse(res, 'Email is already registered', 409);
      }
      throw error;
    }

    const user = await authModel.createFirebaseUser({
      firebaseUid: firebaseUser.uid,
      email: firebaseUser.email ?? email,
      name: email.split('@')[0],
      avatarUrl: null,
      emailVerified: false,
    });

    // Sign in immediately so the client gets a usable session in one call,
    // instead of having to sign in again itself right after registering.
    let session;
    try {
      session = await signInWithPassword(email, password);
    } catch (error) {
      // The account was already created above — this only failed to also
      // hand back a session (e.g. FIREBASE_WEB_API_KEY misconfigured).
      // Tell the client to log in separately instead of a scary 500, but
      // still log the real cause for us to fix.
      if (error instanceof FirebaseRestError) {
        logger.error('Signup succeeded but auto sign-in failed', { email, code: error.code });
        return successResponse(res, 'Registered successfully. Please log in.', { user: toPublicUser(user) }, 201);
      }
      throw error;
    }

    return successResponse(res, 'Registered successfully', { user: toPublicUser(user), ...setSession(res, session) }, 201);
  } catch (error) {
    next(error);
    return;
  }
}

async function login(req: Request, res: Response, next: NextFunction) {
  try {
    const { email, password } = req.body;

    const existing = await authModel.findUserByEmail(email);
    if (!existing) return void errorResponse(res, 'Account does not exist', 404);
    // Invited but not accepted yet: there's no Firebase account to sign in to.
    if (!existing.firebaseUid) {
      return void errorResponse(res, 'Accept your invite first: use the link in your invite email.', 401);
    }
    if (!existing.isActive) return void errorResponse(res, 'This account has been deactivated', 403);

    let session;
    try {
      session = await signInWithPassword(email, password);
    } catch (error) {
      if (error instanceof FirebaseRestError) {
        logger.error('Login sign-in failed', { email, code: error.code });
        return void errorResponse(res, 'Invalid email or password', 401);
      }
      throw error;
    }

    // Checked only after the password, so a suspension isn't revealed to
    // someone who merely knows the email address.
    if (existing.organization && isOrganizationSuspended(existing.organization)) {
      return void tenantSuspendedResponse(res, existing.organization.name);
    }

    await authModel.touchLastLogin(existing.id);
    const sessionUser = await findSessionUser(existing.id);

    // The session (user with effective access + tenant) for the tenant app, plus
    // the tokens in the body for clients that can't read response headers.
    return successResponse(res, 'Logged in successfully', {
      ...(await buildSession(sessionUser!)),
      ...setSession(res, session),
    });
  } catch (error) {
    next(error);
    return;
  }
}

// The client authenticates directly with Firebase; by the time a request
// reaches here, `verifyFirebaseToken` has already verified the token and
// found-or-created the matching local user, so this just returns it.
async function me(req: Request, res: Response, next: NextFunction) {
  try {
    const user = req.user ? await findSessionUser(Number(req.user.sub)) : undefined;
    if (!user) return void errorResponse(res, 'User not found', 404);
    return successResponse(res, 'Current user', await buildSession(user));
  } catch (error) {
    next(error);
    return;
  }
}

function logout(_req: Request, res: Response) {
  // Firebase sessions are client-side (firebase.auth().signOut()); there's
  // nothing server-side to invalidate here today.
  return successResponse(res, 'Logged out successfully');
}

// Exchanges a refresh token for a new id token, once the old one (1 hour
// lifetime) expires — needed by any client with no Firebase SDK of its own
// to do this itself (see setSession() above).
async function refresh(req: Request, res: Response, next: NextFunction) {
  try {
    const { refreshToken } = req.body;
    let session;
    try {
      session = await refreshIdToken(refreshToken);
    } catch (error) {
      if (error instanceof FirebaseRestError) {
        return void errorResponse(res, 'Invalid or expired refresh token', 401);
      }
      throw error;
    }
    // A suspended organization can't keep a session alive by refreshing.
    const user = session.firebaseUid ? await authModel.findUserByFirebaseUid(session.firebaseUid) : null;
    if (user?.organization && isOrganizationSuspended(user.organization)) {
      return void tenantSuspendedResponse(res, user.organization.name);
    }
    return successResponse(res, 'Session refreshed successfully', setSession(res, session));
  } catch (error) {
    next(error);
    return;
  }
}

// PATCH /auth/me: the signed-in person's own name, phone and job title.
async function updateMe(req: Request, res: Response, next: NextFunction) {
  try {
    const id = Number(req.user!.sub);
    const { name, phone, jobTitle } = req.body as UpdateProfileBody;
    const user = await authModel.updateProfile(id, { name, phone: phone || null, jobTitle: jobTitle || null });
    // Best effort: keep the Firebase display name in step; the local row is what the apps show.
    firebaseAuth.updateUser(user.firebaseUid!, { displayName: name }).catch((error: unknown) => {
      logger.error('Failed to update Firebase display name', { message: error instanceof Error ? error.message : String(error) });
    });
    return successResponse(res, 'Profile updated', await buildSession((await findSessionUser(id))!));
  } catch (error) {
    next(error);
    return;
  }
}

// POST /auth/change-password: checks the current password with Firebase, sets the new
// one, and signs in again, since Firebase ends existing sessions on a password change.
// A wrong current password is a 400 on that field (a 401 would end the app's session).
async function changePassword(req: Request, res: Response, next: NextFunction) {
  try {
    const user = await authModel.findUserById(Number(req.user!.sub));
    if (!user?.firebaseUid) return void errorResponse(res, 'User not found', 404);
    const { currentPassword, newPassword } = req.body as ChangePasswordBody;
    try {
      await signInWithPassword(user.email, currentPassword);
    } catch (error) {
      if (error instanceof FirebaseRestError) {
        return void errorResponse(res, 'Your current password is incorrect', 400, [
          { field: 'currentPassword', message: 'Incorrect password' },
        ]);
      }
      throw error;
    }
    await firebaseAuth.updateUser(user.firebaseUid, { password: newPassword });
    const session = await signInWithPassword(user.email, newPassword);
    return successResponse(res, 'Password changed', setSession(res, session));
  } catch (error) {
    next(error);
    return;
  }
}

// Profile photo in S3: orgs/<org>/avatars/<uuid>.<ext> (users/<id>/… without an organization).
async function uploadAvatar(req: Request, res: Response, next: NextFunction) {
  try {
    if (!req.file) return void errorResponse(res, 'Choose an image to upload', 400, [{ field: 'file', message: 'Required' }]);
    const id = Number(req.user!.sub);
    const sessionUser = await findSessionUser(id);
    const prefix = sessionUser?.organizationId ? `orgs/${sessionUser.organizationId}/avatars` : `users/${id}/avatars`;
    const key = await putFile(objectKey(prefix, req.file.originalname), req.file.buffer, imageContentType(req.file.originalname));
    await removeStoredImage(await authModel.setAvatar(id, key));
    return successResponse(res, 'Photo updated', await buildSession((await findSessionUser(id))!));
  } catch (error) {
    next(error);
    return;
  }
}

async function removeAvatar(req: Request, res: Response, next: NextFunction) {
  try {
    const id = Number(req.user!.sub);
    await removeStoredImage(await authModel.setAvatar(id, null));
    return successResponse(res, 'Photo removed', await buildSession((await findSessionUser(id))!));
  } catch (error) {
    next(error);
    return;
  }
}

export { signup, login, me, updateMe, uploadAvatar, removeAvatar, changePassword, logout, refresh };
