namespace MaBibli.Client.Services;
///
/// Parenté des routes de l'application : à quel écran remonte la flèche de retour du bandeau.
///
///
///
/// ⚠️ Ceci renverse la décision actée le 2026-08-20 (« le retour passe par l'historique du
/// navigateur, jamais par une destination calculée »). Le motif d'alors reste vrai — un même
/// écran s'atteint par plusieurs chemins — mais l'historique remonte aussi les
/// allers-retours (filtre, ordre, édition) : on cliquait cinq fois sans quitter le même
/// écran. Une remontée d'un cran de route répond à « où suis-je ? », qui est la question posée.
///
///
/// La table est explicite, et non un découpage naïf de l'URL : toutes les routes n'ont pas
/// la forme d'une arborescence (/souhaits/ordre remonte à /souhaits, mais
/// /auteurs/{id}/bibliographie remonte à /auteurs — la fiche d'un auteur n'existe
/// pas). Une route qui apparaît dans l'application doit apparaître ici.
///
///
/// ⚠️ Le retour ne sort jamais de l'application : la fonction rend toujours un chemin
/// interne, et la racine d'une branche rend la destination de menu correspondante — le catalogue
/// en dernier ressort. C'est ce qui remplace le test de history.length qui vivait en
/// JavaScript, et qui ne disait pas ce qu'on croyait dans une PWA standalone : la pile
/// d'une session y contient aussi ce qui précède l'application.
///
///
public static class RemonteeRoutes
{
/// Le catalogue est la racine : son parent est lui-même, on ne remonte pas plus haut.
public const string Racine = "/";
///
/// Table de parenté, dans l'ordre de lecture. {id} représente un segment numérique.
///
///
/// La première ligne dont le modèle correspond gagne : les modèles les plus longs sont donc
/// écrits avant ceux dont ils sont un prolongement.
///
private static readonly (string Modele, string Parent)[] Parents =
[
// Fiche livre : l'édition retombe sur la consultation, la consultation sur le catalogue.
("/livres/{id}/edition", "/livres/{id}"),
("/livres/{id}", Racine),
// Séries : les deux écrans de modification retombent sur la consultation de la série.
("/series/{id}/edition", "/series/{id}"),
("/series/{id}/ordre", "/series/{id}"),
("/series/{id}", "/series"),
("/series", Racine),
// Envies : l'ordre, l'ajout et les deux écrans d'édition sont des écrans de la même
// liste. ⚠️ « /souhaits/revues/{id}/edition » ne peut pas se confondre avec
// « /souhaits/{id}/edition » : les modèles n'ont pas le même nombre de segments.
("/souhaits/revues/{id}/edition", "/souhaits"),
("/souhaits/{id}/edition", "/souhaits"),
("/souhaits/ordre", "/souhaits"),
("/souhaits/ajout", "/souhaits"),
("/souhaits", Racine),
// Ajout d'un ouvrage : on y entre depuis le catalogue comme depuis les revues, mais le
// catalogue est la destination de menu de ce qu'on y saisit.
("/ajout/manuel", Racine),
("/ajout/isbn", Racine),
("/ajout", Racine),
// Revues. ⚠️ « /revues/ajout » est écrit AVANT « /revues/{id} » : « ajout » n'est pas un
// identifiant, mais l'ordre de lecture est ce qui le garantit sans ambiguïté.
("/revues/ajout", "/revues"),
("/revues/{id}/edition", "/revues/{id}"),
("/revues/{id}", "/revues"),
("/revues", Racine),
// La bibliographie remonte à la LISTE des auteurs : il n'existe pas de fiche auteur.
("/auteurs/{id}/bibliographie", "/auteurs"),
("/auteurs", Racine),
("/prets", Racine),
// « À propos » est une annexe du menu : elle remonte au catalogue, comme toute racine de
// branche. Elle n'a pas d'enfants, et n'en aura pas.
("/a-propos", Racine),
("/not-found", Racine),
];
///
/// Écran d'où l'on vient hiérarchiquement, à partir d'un chemin d'application.
///
///
/// Chemin absolu ou relatif à la base, avec ou sans requête (?auteur=3) ni fragment.
///
/// Un chemin interne, toujours ; jamais null, jamais une URL absolue.
public static string Parent(string? chemin)
{
var segments = Segments(chemin);
if (segments.Length == 0)
{
return Racine;
}
foreach (var (modele, parent) in Parents)
{
if (Correspond(Segments(modele), segments))
{
return Rendre(parent, segments);
}
}
// Route inconnue : on retombe sur la destination de menu de sa branche, plutôt que sur
// un chemin deviné. Une route ajoutée sans sa ligne de table reste ainsi utilisable —
// mais elle doit être ajoutée : c'est un repli, pas le mécanisme.
return Parents.FirstOrDefault(p => p.Modele == $"/{segments[0]}").Parent is not null
? $"/{segments[0]}"
: Racine;
}
/// Découpe en segments non vides, requête et fragment retirés.
private static string[] Segments(string? chemin)
{
if (string.IsNullOrWhiteSpace(chemin))
{
return [];
}
var utile = chemin.Split('?', '#')[0];
return utile.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
}
private static bool Correspond(string[] modele, string[] segments)
{
if (modele.Length != segments.Length)
{
return false;
}
for (var i = 0; i < modele.Length; i++)
{
var attendu = modele[i];
var ok = attendu == "{id}"
? int.TryParse(segments[i], out _)
: string.Equals(attendu, segments[i], StringComparison.OrdinalIgnoreCase);
if (!ok)
{
return false;
}
}
return true;
}
/// Remplace {id} du parent par l'identifiant lu dans le chemin d'origine.
private static string Rendre(string parent, string[] segments)
{
if (!parent.Contains("{id}", StringComparison.Ordinal))
{
return parent;
}
var identifiant = segments.FirstOrDefault(s => int.TryParse(s, out _));
// Sans identifiant lisible, on ne fabrique pas une route bancale : la racine est sûre.
return identifiant is null
? Racine
: parent.Replace("{id}", identifiant, StringComparison.Ordinal);
}
}