/*
  Mini-application météo en JavaScript natif.

  Objectif pédagogique :
  montrer le trajet complet d’une donnée dans une interface web simple :

  formulaire
  → événement JavaScript
  → requête HTTP avec fetch()
  → API
  → réponse JSON
  → mise à jour du DOM
  → gestion des erreurs
*/

// -----------------------------------------------------------------------------
// 1. Démarrage de l’application
// -----------------------------------------------------------------------------

/*
  DOMContentLoaded garantit que le HTML est chargé avant de chercher
  des éléments dans la page avec document.querySelector().
*/
document.addEventListener('DOMContentLoaded', function () {
  _app();
});

/*
  La fonction _app() regroupe toute la logique de cette petite application.

  Pour un exemple pédagogique, cela permet de garder le code au même endroit,
  tout en évitant de placer directement toute la logique dans l’écouteur
  DOMContentLoaded.
*/
function _app() {
  // ---------------------------------------------------------------------------
  // 2. Références vers les éléments HTML
  // ---------------------------------------------------------------------------

  /*
    JavaScript garde ici des références vers les éléments importants du DOM.

    Ces éléments existent déjà dans index.html.
    Le script ne crée donc pas toute l’interface : il la rend interactive.
  */
  const form = document.querySelector('#weather-form');
  const cityInput = document.querySelector('#city-input');
  const searchButton = document.querySelector('#search-button');

  /*
    Zone de message et bloc de résultat.

    Ces éléments permettent d’afficher les différents états de l’interface :
    aide initiale, chargement, succès, erreur ou résultat météo.
  */
  const message = document.querySelector('#message');
  const result = document.querySelector('#weather-result');

  /*
    Chaque donnée météo affichée possède son propre emplacement dans le HTML.

    Ce choix rend le code très explicite :
    une donnée reçue de l’API correspond à une zone précise dans la page.
  */
  const resultCity = document.querySelector('#result-city');
  const resultTemperature = document.querySelector('#result-temperature');
  const resultDescription = document.querySelector('#result-description');
  const resultApparentTemperature = document.querySelector('#result-apparent-temperature');
  const resultHumidity = document.querySelector('#result-humidity');
  const resultWind = document.querySelector('#result-wind');
  const resultGusts = document.querySelector('#result-gusts');
  const resultPressure = document.querySelector('#result-pressure');
  const resultClouds = document.querySelector('#result-clouds');
  const resultPrecipitation = document.querySelector('#result-precipitation');
  const resultDay = document.querySelector('#result-day');
  const resultTime = document.querySelector('#result-time');

  // ---------------------------------------------------------------------------
  // 3. Événement principal : soumission du formulaire
  // ---------------------------------------------------------------------------

  /*
    Le formulaire est le point d’entrée principal.

    L’utilisateur saisit une ville, puis déclenche un événement submit.
    JavaScript intercepte cet événement pour lancer la recherche météo
    sans recharger la page.
  */
  form.addEventListener('submit', async function (event) {
    event.preventDefault();

    /*
      trim() retire les espaces inutiles au début et à la fin.
      Cela évite qu’une saisie comme " Paris " soit traitée comme différente.
    */
    const cityName = cityInput.value.trim();

    /*
      Avant chaque nouvelle recherche, on remet l’interface dans un état clair :
      l’ancien résultat est masqué et le message est réinitialisé.
    */
    resetResult();

    /*
      Première erreur possible : le champ est vide.

      Dans ce cas, il est inutile d’appeler l’API.
      L’erreur est locale et peut être traitée immédiatement.
    */
    if (cityName === '') {
      showMessage('Veuillez saisir le nom d’une ville.', 'error');
      cityInput.focus();
      return;
    }

    /*
      L’état de chargement indique qu’une opération est en cours.
      Le champ et le bouton sont temporairement désactivés pour éviter
      plusieurs recherches simultanées.
    */
    setLoadingState(true);

    try {
      /*
        Étape 1 :
        transformer le nom de ville saisi par l’utilisateur en coordonnées.

        Une API météo travaille généralement avec une latitude et une longitude,
        pas seulement avec un nom de ville.
      */
      const location = await findCity(cityName);

      /*
        La requête de géocodage peut réussir techniquement,
        mais ne trouver aucune ville correspondante.

        Ce n’est pas une erreur réseau :
        c’est un cas fonctionnel à gérer proprement.
      */
      if (!location) {
        showMessage('Aucune ville trouvée pour cette recherche.', 'error');
        return;
      }

      /*
        Étape 2 :
        utiliser les coordonnées trouvées pour demander la météo actuelle.
      */
      const weather = await findCurrentWeather(location);

      /*
        Étape 3 :
        afficher dans la page les données reçues de l’API.
      */
      showWeather(location, weather);

      showMessage('Météo chargée avec succès.', 'success');
    } catch (error) {
      /*
        catch() regroupe les erreurs techniques :
        problème réseau, API indisponible, réponse inattendue, etc.

        console.error() aide le développeur.
        showMessage() informe l’utilisateur avec un message compréhensible.
      */
      console.error(error);

      showMessage(
        'Impossible de récupérer la météo pour le moment. Vérifiez votre connexion ou réessayez plus tard.',
        'error'
      );
    } finally {
      /*
        finally() s’exécute dans tous les cas :
        succès, ville introuvable ou erreur.

        Il garantit que l’interface ne reste jamais bloquée
        en état de chargement.
      */
      setLoadingState(false);
    }
  });

  // ---------------------------------------------------------------------------
  // 4. Géocodage : transformer une ville en coordonnées
  // ---------------------------------------------------------------------------

  /*
    findCity() interroge l’API de géocodage d’Open-Meteo.

    Entrée :
    - un nom de ville saisi par l’utilisateur.

    Sortie :
    - un objet location contenant notamment name, country, latitude et longitude ;
    - ou null si aucune ville n’est trouvée.
  */
  async function findCity(cityName) {
    /*
      URL permet de construire une adresse proprement.
      searchParams évite de concaténer manuellement les paramètres de requête.
    */
    const url = new URL('https://geocoding-api.open-meteo.com/v1/search');

    url.searchParams.set('name', cityName);
    url.searchParams.set('count', '1');
    url.searchParams.set('language', 'fr');
    url.searchParams.set('format', 'json');

    /*
      fetch() envoie une requête HTTP.
      await attend la réponse avant de continuer.
    */
    const response = await fetch(url);

    /*
      Une réponse HTTP peut arriver, mais signaler une erreur :
      404, 500, etc.

      response.ok permet de vérifier que le statut HTTP est dans la plage succès.
    */
    if (!response.ok) {
      throw new Error('Erreur lors du géocodage de la ville.');
    }

    /*
      La réponse HTTP est convertie en objet JavaScript à partir du JSON reçu.
    */
    const data = await response.json();

    /*
      Si l’API ne trouve aucune ville, elle ne renvoie pas de résultat exploitable.
      On retourne null pour permettre au code appelant de gérer ce cas.
    */
    if (!data.results || data.results.length === 0) {
      return null;
    }

    /*
      Pour cette version simple, on prend le premier résultat.
      Une version plus avancée pourrait proposer plusieurs villes à l’utilisateur.
    */
    return data.results[0];
  }

  // ---------------------------------------------------------------------------
  // 5. Requête météo : récupérer les données actuelles
  // ---------------------------------------------------------------------------

  /*
    findCurrentWeather() interroge l’API météo d’Open-Meteo.

    Entrée :
    - un objet location contenant latitude et longitude.

    Sortie :
    - un objet weather contenant les données météo actuelles.
  */
  async function findCurrentWeather(location) {
    const url = new URL('https://api.open-meteo.com/v1/forecast');

    /*
      Les coordonnées géographiques proviennent de la première requête.
      C’est le lien entre le géocodage et la météo.
    */
    url.searchParams.set('latitude', location.latitude);
    url.searchParams.set('longitude', location.longitude);

    /*
      Le paramètre current liste les données météo souhaitées.

      L’API pourrait fournir davantage d’informations, mais cette sélection
      suffit à créer une interface riche tout en restant lisible.
    */
    url.searchParams.set(
      'current',
      [
        'temperature_2m',
        'relative_humidity_2m',
        'apparent_temperature',
        'weather_code',
        'wind_speed_10m',
        'wind_direction_10m',
        'wind_gusts_10m',
        'pressure_msl',
        'cloud_cover',
        'precipitation',
        'rain',
        'showers',
        'snowfall',
        'is_day'
      ].join(',')
    );

    /*
      timezone=auto demande à l’API d’adapter l’heure retournée
      au fuseau horaire du lieu recherché.
    */
    url.searchParams.set('timezone', 'auto');

    const response = await fetch(url);

    if (!response.ok) {
      throw new Error('Erreur lors de la récupération de la météo.');
    }

    const data = await response.json();

    /*
      On vérifie que la structure reçue contient bien le bloc attendu.
      Cette vérification protège l’interface contre une réponse incomplète
      ou modifiée.
    */
    if (!data.current) {
      throw new Error('Réponse météo inattendue.');
    }

    return data.current;
  }

  // ---------------------------------------------------------------------------
  // 6. Affichage du résultat dans le DOM
  // ---------------------------------------------------------------------------

  /*
    showWeather() transforme les données reçues en texte lisible,
    puis les injecte dans les éléments HTML prévus.

    C’est le passage central :
    JSON reçu → valeurs formatées → DOM mis à jour.
  */
  function showWeather(location, weather) {
    const cityLabel = buildCityLabel(location);
    const precipitationLabel = buildPrecipitationLabel(weather);

    resultCity.textContent = cityLabel;
    resultTemperature.textContent = formatTemperature(weather.temperature_2m);
    resultDescription.textContent = buildWeatherSummary(weather);

    resultApparentTemperature.textContent = formatTemperature(weather.apparent_temperature);
    resultHumidity.textContent = formatPercent(weather.relative_humidity_2m);
    resultWind.textContent = buildWindLabel(weather);
    resultGusts.textContent = formatSpeed(weather.wind_gusts_10m);
    resultPressure.textContent = `${Math.round(weather.pressure_msl)} hPa`;
    resultClouds.textContent = formatPercent(weather.cloud_cover);
    resultPrecipitation.textContent = precipitationLabel;
    resultDay.textContent = formatDayMoment(weather.is_day);
    resultTime.textContent = formatTime(weather.time);

    /*
      Le résultat était masqué par l’attribut hidden.
      On l’affiche uniquement lorsque les données sont prêtes.
    */
    result.hidden = false;
  }

  // ---------------------------------------------------------------------------
  // 7. Construction de libellés lisibles
  // ---------------------------------------------------------------------------

  /*
    Le géocodage peut renvoyer plusieurs informations :
    nom, région administrative, pays.

    On assemble uniquement les parties disponibles.
  */
  function buildCityLabel(location) {
    const parts = [
      location.name,
      location.admin1,
      location.country
    ].filter(Boolean);

    return parts.join(', ');
  }

  /*
    Open-Meteo renvoie un code météo numérique.
    Cette fonction traduit ce code en description compréhensible.
  */
  function getWeatherDescription(weatherCode) {
    const descriptions = {
      0: 'Ciel dégagé',
      1: 'Principalement dégagé',
      2: 'Partiellement nuageux',
      3: 'Couvert',
      45: 'Brouillard',
      48: 'Brouillard givrant',
      51: 'Bruine légère',
      53: 'Bruine modérée',
      55: 'Bruine dense',
      61: 'Pluie faible',
      63: 'Pluie modérée',
      65: 'Pluie forte',
      71: 'Neige faible',
      73: 'Neige modérée',
      75: 'Neige forte',
      80: 'Averses faibles',
      81: 'Averses modérées',
      82: 'Averses violentes',
      95: 'Orage',
      96: 'Orage avec grêle faible',
      99: 'Orage avec grêle forte'
    };

    return descriptions[weatherCode] || 'Conditions météo non précisées';
  }

  /*
    Le résumé météo combine deux informations :
    - la description liée au code météo ;
    - le moment jour / nuit.
  */
  function buildWeatherSummary(weather) {
    const description = getWeatherDescription(weather.weather_code);
    const dayMoment = weather.is_day === 1 ? 'jour' : 'nuit';

    return `${description} · ${dayMoment}`;
  }

  /*
    Le vent est plus lisible lorsque la vitesse et la direction
    sont affichées ensemble.
  */
  function buildWindLabel(weather) {
    const speed = formatSpeed(weather.wind_speed_10m);
    const direction = getWindDirection(weather.wind_direction_10m);

    return `${speed} · ${direction}`;
  }

  /*
    Les précipitations peuvent être réparties entre plusieurs champs :
    pluie, averses, neige ou précipitation globale.

    Cette fonction choisit le libellé le plus parlant pour l’utilisateur.
  */
  function buildPrecipitationLabel(weather) {
    const precipitation = Number(weather.precipitation || 0);
    const rain = Number(weather.rain || 0);
    const showers = Number(weather.showers || 0);
    const snowfall = Number(weather.snowfall || 0);

    if (snowfall > 0) {
      return `${formatMillimeters(snowfall)} neige`;
    }

    if (rain > 0) {
      return `${formatMillimeters(rain)} pluie`;
    }

    if (showers > 0) {
      return `${formatMillimeters(showers)} averses`;
    }

    if (precipitation > 0) {
      return formatMillimeters(precipitation);
    }

    return 'Aucune';
  }

  /*
    L’API renvoie la direction du vent en degrés.
    Pour une interface simple, on convertit ces degrés en points cardinaux.
  */
  function getWindDirection(degrees) {
    if (degrees === null || degrees === undefined) {
      return 'direction inconnue';
    }

    const directions = [
      'N',
      'NE',
      'E',
      'SE',
      'S',
      'SO',
      'O',
      'NO'
    ];

    /*
      Chaque direction couvre environ 45 degrés.
      Le modulo permet de revenir à 0 lorsque l’on dépasse le dernier index.
    */
    const index = Math.round(degrees / 45) % 8;

    return directions[index];
  }

  // ---------------------------------------------------------------------------
  // 8. Fonctions de formatage
  // ---------------------------------------------------------------------------

  /*
    Les fonctions de formatage évitent de mélanger calculs, unités
    et mise à jour du DOM.

    Elles rendent aussi le comportement plus cohérent :
    si une valeur manque, on affiche toujours le même symbole.
  */
  function formatTemperature(value) {
    if (value === null || value === undefined) {
      return '—';
    }

    return `${Math.round(value)} °C`;
  }

  function formatSpeed(value) {
    if (value === null || value === undefined) {
      return '—';
    }

    return `${Math.round(value)} km/h`;
  }

  function formatPercent(value) {
    if (value === null || value === undefined) {
      return '—';
    }

    return `${Math.round(value)} %`;
  }

  function formatDayMoment(value) {
    if (value === 1) {
      return 'Jour';
    }

    if (value === 0) {
      return 'Nuit';
    }

    return '—';
  }

  function formatMillimeters(value) {
    if (value === null || value === undefined) {
      return '—';
    }

    return `${Number(value).toFixed(1)} mm`;
  }

  /*
    Intl.DateTimeFormat formate l’heure selon une locale donnée.
    Ici, on utilise le format français sans écrire soi-même la logique d’affichage.
  */
  function formatTime(value) {
    if (!value) {
      return '—';
    }

    return new Intl.DateTimeFormat('fr-FR', {
      hour: '2-digit',
      minute: '2-digit'
    }).format(new Date(value));
  }

  // ---------------------------------------------------------------------------
  // 9. États et messages d’interface
  // ---------------------------------------------------------------------------

  /*
    showMessage() centralise l’affichage des messages.

    Le texte est placé dans le DOM avec textContent.
    Le type est stocké dans data-type, ce qui permet au CSS de changer
    l’apparence du message.
  */
  function showMessage(text, type) {
    message.textContent = text;

    if (type) {
      message.dataset.type = type;
    } else {
      delete message.dataset.type;
    }
  }

  /*
    resetResult() prépare l’interface avant une nouvelle recherche.

    Le résultat précédent est masqué et chaque champ est vidé.
    Cela évite d’afficher d’anciennes données pendant la nouvelle requête.
  */
  function resetResult() {
    result.hidden = true;

    resultCity.textContent = '';
    resultTemperature.textContent = '';
    resultDescription.textContent = '';
    resultApparentTemperature.textContent = '';
    resultHumidity.textContent = '';
    resultWind.textContent = '';
    resultGusts.textContent = '';
    resultPressure.textContent = '';
    resultClouds.textContent = '';
    resultPrecipitation.textContent = '';
    resultDay.textContent = '';
    resultTime.textContent = '';

    showMessage('Recherche en cours...', null);
  }

  /*
    setLoadingState() rend visible l’état de chargement.

    Pendant une requête :
    - le champ est désactivé ;
    - le bouton est désactivé ;
    - le texte du bouton change.

    Cela limite les actions ambiguës pendant l’attente de la réponse API.
  */
  function setLoadingState(isLoading) {
    searchButton.disabled = isLoading;
    cityInput.disabled = isLoading;

    searchButton.textContent = isLoading
      ? 'Recherche...'
      : 'Rechercher';
  }
}