import * as Crypto from "expo-crypto";
import * as SecureStore from "expo-secure-store";
import argon2 from "react-native-argon2";
import type { MobileUser } from "../types/auth";

/**
 * Credential de reconnexion HORS-LIGNE (point D de l'audit offline).
 *
 * Objectif : permettre a un technicien deja connecte une fois EN LIGNE sur une
 * instance de rouvrir une session SANS reseau (typiquement : il s'est
 * deconnecte le soir, et le lendemain matin dans le camion sans signal il veut
 * acceder a ses interventions du jour).
 *
 * Securite (mot de passe) : on ne stocke JAMAIS le mot de passe. On garde un
 * hash **Argon2id** (etat de l'art, gagnant de la Password Hashing
 * Competition), avec les MEMES parametres que la PWA (PasswordHasher.php :
 * memory_cost=65536 / time_cost=4 / threads=1). L'ensemble vit en plus dans
 * SecureStore (Keychain iOS / Keystore Android, chiffre materiel). Les donnees
 * metier (non sensibles : releves CVC) restent en clair dans SQLite.
 *
 * /!\ react-native-argon2 est un module NATIF : tout changement ici exige un
 * REBUILD APK (npm run build:test/prod), il N'EST PAS livrable en OTA.
 *
 * Borne : TTL de 30 jours. Au-dela, reconnexion en ligne exigee.
 */

const KEY = "cc_offline_credential";
const TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 jours

// Parametres Argon2id alignes sur la PWA (coolcare-app PasswordHasher.php).
const ARGON2_OPTIONS = {
  memory: 65536, // 64 Mio (minimum OWASP)
  iterations: 4,
  parallelism: 1,
  hashLength: 32,
  mode: "argon2id" as const,
  saltEncoding: "hex" as const,
};

type OfflineCredentialRecord = {
  baseUrl: string;
  email: string; // normalise en minuscules
  salt: string; // hex
  encodedHash: string; // $argon2id$v=19$m=65536,t=4,p=1$...
  user: MobileUser;
  savedAt: number; // epoch ms
};

async function randomSaltHex(): Promise<string> {
  const bytes = await Crypto.getRandomBytesAsync(16);
  return Array.from(bytes)
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
}

async function hashPassword(password: string, saltHex: string): Promise<string> {
  const result = await argon2(password, saltHex, ARGON2_OPTIONS);
  return result.encodedHash;
}

// Comparaison a temps constant : evite une fuite par timing sur l'egalite du
// hash (defense en profondeur ; impact reel faible en local).
function timingSafeEqual(a: string, b: string): boolean {
  if (a.length !== b.length) {
    return false;
  }
  let diff = 0;
  for (let i = 0; i < a.length; i += 1) {
    diff |= (a.codePointAt(i) ?? 0) ^ (b.codePointAt(i) ?? 0);
  }
  return diff === 0;
}

function normalizeEmail(email: string): string {
  return email.trim().toLowerCase();
}

/**
 * Memorise le credential offline apres un login EN LIGNE reussi (on a alors le
 * mot de passe en clair, jamais persiste). Un seul enregistrement a la fois :
 * la derniere connexion ecrase la precedente (coherent avec le modele
 * single-session / owner du cache local).
 */
export async function saveOfflineCredential(
  baseUrl: string,
  email: string,
  password: string,
  user: MobileUser
): Promise<void> {
  const salt = await randomSaltHex();
  const encodedHash = await hashPassword(password, salt);
  const record: OfflineCredentialRecord = {
    baseUrl,
    email: normalizeEmail(email),
    salt,
    encodedHash,
    user,
    savedAt: Date.now(),
  };
  await SecureStore.setItemAsync(KEY, JSON.stringify(record));
}

/**
 * Verifie un couple email/mot de passe hors-ligne. Retourne l'utilisateur
 * memorise si tout concorde (meme instance, meme email, hash Argon2id valide,
 * non expire), sinon null. Ne leve jamais (best-effort cote appelant).
 */
export async function verifyOfflineCredential(
  baseUrl: string,
  email: string,
  password: string
): Promise<MobileUser | null> {
  const raw = await SecureStore.getItemAsync(KEY);
  if (!raw) {
    return null;
  }

  let rec: OfflineCredentialRecord;
  try {
    rec = JSON.parse(raw) as OfflineCredentialRecord;
  } catch {
    return null;
  }

  if (rec.baseUrl !== baseUrl) {
    return null;
  }
  if (rec.email !== normalizeEmail(email)) {
    return null;
  }
  if (!rec.savedAt || Date.now() - rec.savedAt > TTL_MS) {
    return null;
  }

  // Recompute avec le MEME sel + memes parametres : Argon2id est deterministe a
  // sel fixe, donc l'encodedHash recalcule doit egaler celui stocke.
  let candidate: string;
  try {
    candidate = await hashPassword(password, rec.salt);
  } catch {
    return null;
  }
  if (!timingSafeEqual(candidate, rec.encodedHash)) {
    return null;
  }

  return rec.user;
}

export async function clearOfflineCredential(): Promise<void> {
  await SecureStore.deleteItemAsync(KEY);
}
