import type { Context } from 'hono';
import { Hono } from 'hono';

import {
  AuthError,
  type AuthenticatedUser,
  type AuthService,
  type IssuedSession,
  authService,
} from './service.js';

type JsonObject = Record<string, unknown>;

function objectValue(value: unknown): JsonObject | null {
  return value !== null && typeof value === 'object' && !Array.isArray(value)
    ? (value as JsonObject)
    : null;
}

async function readJsonObject(context: Context): Promise<JsonObject | null> {
  try {
    return objectValue(await context.req.json<unknown>());
  } catch {
    return null;
  }
}

function normalizeHeaderToken(value: string | undefined): string | null {
  if (!value) {
    return null;
  }

  const trimmed = value.trim();
  const bearer = /^Bearer\s+(\S+)$/i.exec(trimmed);
  const token = bearer?.[1] ?? trimmed;
  return token.length > 0 && token.length <= 8_192 ? token : null;
}

export function accessTokenFromRequest(context: Context): string | null {
  // The ERP historically sends the Supabase anon key in Authorization and the
  // actual user token in X-User-Token, so the custom header has precedence.
  return (
    normalizeHeaderToken(context.req.header('X-User-Token')) ??
    normalizeHeaderToken(context.req.header('Authorization'))
  );
}

function publicUser(user: AuthenticatedUser): JsonObject {
  const profile = user.profile;
  const metadata = user.metadata;
  const level =
    (typeof profile.nivel === 'string' && profile.nivel) ||
    (typeof metadata.nivel === 'string' && metadata.nivel) ||
    'usuario';
  const type =
    (typeof profile.tipo === 'string' && profile.tipo) ||
    (typeof metadata.tipo === 'string' && metadata.tipo) ||
    level;

  return {
    id: typeof profile.id === 'string' ? profile.id : user.id,
    authUserId: user.id,
    userId: user.id,
    email: user.email,
    nome:
      (typeof profile.nome === 'string' && profile.nome) ||
      (typeof metadata.nome === 'string' && metadata.nome) ||
      null,
    empresaId:
      (typeof profile.empresaId === 'string' && profile.empresaId) ||
      (typeof metadata.empresaId === 'string' && metadata.empresaId) ||
      (typeof metadata.empresa_id === 'string' && metadata.empresa_id) ||
      null,
    tipo: type,
    nivel: level,
    nomeEmpresa: user.companyName,
    permissoes: objectValue(profile.permissoes) ?? {},
    modulosPermitidos: Array.isArray(profile.modulosPermitidos)
      ? profile.modulosPermitidos
      : [],
  };
}

function sessionResponse(session: IssuedSession, message: string): JsonObject {
  const user = publicUser(session.user);

  return {
    success: true,
    message,
    data: {
      ...user,
      accessToken: session.accessToken,
      refreshToken: session.refreshToken,
      tokenType: session.tokenType,
      expiresIn: session.expiresIn,
      expiresAt: session.expiresAt,
      refreshExpiresAt: session.refreshExpiresAt,
      sessionId: session.sessionId,
    },
    // Supabase-shaped aliases make the transition easier for clients that
    // already persist the former SDK session object.
    session: {
      access_token: session.accessToken,
      refresh_token: session.refreshToken,
      token_type: session.tokenType.toLowerCase(),
      expires_in: session.expiresIn,
      expires_at: Math.floor(new Date(session.expiresAt).getTime() / 1_000),
      user,
    },
  };
}

function errorResponse(error: unknown, context: Context): Response {
  if (error instanceof AuthError) {
    return context.json(
      {
        success: false,
        error: error.message,
        code: error.code,
        needsLogin: error.status === 401,
      },
      error.status,
    );
  }

  return context.json(
    {
      success: false,
      error: 'Erro interno no servidor. Tente novamente.',
      code: 'internal_error',
    },
    500,
  );
}

export function createAuthRoutes(service: AuthService = authService): Hono {
  const router = new Hono();

  router.post('/signin', async (context) => {
    const body = await readJsonObject(context);
    if (!body) {
      return context.json(
        { success: false, error: 'Pedido JSON inválido.', code: 'invalid_json' },
        400,
      );
    }

    try {
      const session = await service.signIn(body.email, body.password, {
        userAgent: context.req.header('User-Agent'),
      });
      return context.json(sessionResponse(session, 'Login realizado com sucesso'));
    } catch (error) {
      return errorResponse(error, context);
    }
  });

  router.post('/refresh', async (context) => {
    const body = await readJsonObject(context);
    if (!body) {
      return context.json(
        { success: false, error: 'Pedido JSON inválido.', code: 'invalid_json' },
        400,
      );
    }

    try {
      const refreshToken = body.refreshToken ?? body.refresh_token;
      const session = await service.refresh(refreshToken, {
        userAgent: context.req.header('User-Agent'),
      });
      return context.json(sessionResponse(session, 'Sessão renovada com sucesso'));
    } catch (error) {
      return errorResponse(error, context);
    }
  });

  router.post('/signout', async (context) => {
    const body = await readJsonObject(context);
    const refreshToken = body?.refreshToken ?? body?.refresh_token;

    try {
      await service.signOut(accessTokenFromRequest(context), refreshToken);
      return context.json({
        success: true,
        message: 'Logout realizado com sucesso',
      });
    } catch (error) {
      return errorResponse(error, context);
    }
  });

  const getUser = async (context: Context): Promise<Response> => {
    let accessToken = accessTokenFromRequest(context);

    // Body tokens keep POST /user usable by the former /auth/me call pattern,
    // while header-based authentication remains the primary contract.
    if (!accessToken && context.req.method === 'POST') {
      const body = await readJsonObject(context);
      accessToken = normalizeHeaderToken(
        typeof body?.token === 'string' ? body.token : undefined,
      );
    }

    if (!accessToken) {
      return context.json(
        {
          success: false,
          error: 'Token de acesso não fornecido.',
          code: 'token_required',
          needsLogin: true,
        },
        401,
      );
    }

    try {
      const user = publicUser(await service.getUser(accessToken));
      return context.json({ success: true, user, data: user });
    } catch (error) {
      return errorResponse(error, context);
    }
  };

  router.get('/user', getUser);
  router.post('/user', getUser);

  return router;
}

export const authRoutes = createAuthRoutes();
export default authRoutes;
