#!/usr/bin/env dotnet-script

#nullable enable
#r "nuget: Microsoft.Data.Sqlite, 7.0.4"

using System.Globalization;
using System.Net;
using Microsoft.Data.Sqlite;
using SQLitePCL;

private readonly string BASE_PATH = Path.GetFullPath("..");
private readonly string OUTPUT_DIR = Path.Combine(BASE_PATH, "site", "partial");
private readonly string CONNECTION_STRING =
  $"Data Source={Path.Combine(BASE_PATH, "db", "radiostasis.db")}";

private class Series {
  public required string Slug { get; set; }
  public required string Title { get; set; }
  public required string TitleSort { get; set; }
  public required DateTime DateAdded { get; set; }
  public required short MinYear { get; set; }
  public required short MaxYear { get; set; }
  public required IEnumerable<string> Tags { get; set; }
  public string? Description { get; set; }
  public required int EpisodeCount { get; set; }

  public string YearRange {
    get => MinYear == MaxYear
      ? $"{MinYear}"
      : $"{MinYear}-{MaxYear}";
  }

  public string SlugEncoded {
    get => WebUtility.HtmlEncode(Slug);
  }

  public string TitleEncoded {
    get => WebUtility.HtmlEncode(Title);
  }
}

private class Episode {
  public required string Slug { get; set; }
  public required string Title { get; set; }
  public required string FileName { get; set; }
  public required long FileSize { get; set; }
  public required int Length { get; set; }
  public string? AirDate { get; set; }

  public string SlugEncoded {
    get => WebUtility.HtmlEncode(Slug);
  }

  public string TitleEncoded {
    get => WebUtility.HtmlEncode(Title);
  }

  public string FileNameEncoded {
    get => WebUtility.HtmlEncode(FileName);
  }

  public string LengthDisplay {
    get => $"{Math.Round(Length / 60f)}mins";
  }

  public string AirDateDisplay {
    get => !string.IsNullOrWhiteSpace(AirDate)
      ? $"Aired {AirDate}"
      : "Air date unknown";
  }
}

private IEnumerable<string> GetTags() {
  using var connection = new SqliteConnection(CONNECTION_STRING);
  connection.Open();

  var tags = new List<string>();
  using var cmd = connection.CreateCommand();
  cmd.CommandText = "select distinct tag from series_tags order by tag";
  using var reader = cmd.ExecuteReader();
  while (reader.Read()) {
    tags.Add(reader.GetString(0));
  }

  return tags;
}

private IEnumerable<Series> GetSeries() {
  using var connection = new SqliteConnection(CONNECTION_STRING);
  connection.Open();

  // get all series tags
  var tags = new Dictionary<string, List<string>>();
  using (var tagcmd = connection.CreateCommand()) {
    tagcmd.CommandText = "select series_slug, tag from series_tags order by tag";
    using var tagreader = tagcmd.ExecuteReader();
    while (tagreader.Read()) {
      var slug = tagreader.GetString(0);
      var tag = tagreader.GetString(1);
      if (!tags.ContainsKey(slug)) tags.Add(slug, new());
      tags[slug].Add(tag);
    }
  }

  // get all series
  using var cmd = connection.CreateCommand();
  cmd.CommandText =
      @"select
          s.series_slug,
          s.title,
          s.date_added,
          s.min_year,
          s.max_year,
          s.description,
          s.title_sort,
          count(e.episode_slug) episode_count
        from series s
        inner join episodes e on
          e.series_slug=s.series_slug
        group by s.series_slug
        order by s.title_sort";

  var series = new List<Series>();
  using var reader = cmd.ExecuteReader();
  while (reader.Read()) {
    var slug = reader.GetString(0);
    series.Add(new() {
      Slug = slug,
      Title = reader.GetString(1),
      DateAdded = reader.GetDateTime(2),
      MinYear = reader.GetInt16(3),
      MaxYear = reader.GetInt16(4),
      Description = reader.IsDBNull(5) ? null : reader.GetString(5),
      TitleSort = reader.GetString(6),
      EpisodeCount = reader.GetInt32(7),
      Tags = tags[slug],
    });
  }

  return series;
}

private IEnumerable<Episode> GetEpisodes(string seriesSlug) {
  using var connection = new SqliteConnection(CONNECTION_STRING);
  connection.Open();

  using var cmd = connection.CreateCommand();
  cmd.CommandText =
    @"select episode_slug, title, file_name, file_size, episode_length, air_date
      from episodes where series_slug=@Series
      order by episode_slug";
  var param = cmd.CreateParameter();
  param.ParameterName = "Series";
  param.Value = seriesSlug;
  cmd.Parameters.Add(param);
  var episodes = new List<Episode>();
  using var reader = cmd.ExecuteReader();
  while (reader.Read()) {
    episodes.Add(new() {
      Slug = reader.GetString(0),
      Title = reader.GetString(1),
      FileName = reader.GetString(2),
      FileSize = reader.GetInt64(3),
      Length = reader.GetInt32(4),
      AirDate = reader.IsDBNull(5) ? null : reader.GetString(5),
    });
  }

  return episodes;
}

private string DisplayTag(string tag) =>
  CultureInfo.CurrentCulture.TextInfo.ToTitleCase(tag.ToLower().Replace("-", " "))
    .Replace(" ", "-");

private void GenerateSeriesListFragment(
    IEnumerable<Series> seriesList, string path, string title) {
  using var outStream = File.Open(path, FileMode.Create);
  using var sw = new StreamWriter(outStream);
  sw.Write(
      @$"<h2>{WebUtility.HtmlEncode(title)}</h2>
      <div class='seriesList'>");

  foreach (var series in seriesList) {
    sw.Write(@$"
        <section
            hx-get='/partial/series/{series.SlugEncoded}.html'
            hx-target='main'
            hx-push-url='/series/{series.SlugEncoded}'
            hx-swap='innerHTML show:top'
            title='{series.TitleEncoded}'>
          <img alt='cover image' title='{series.TitleEncoded}'
              src='/cover/sm/{series.SlugEncoded}.jpg'>
          <div>
            <ul>
              {string.Join(string.Empty, series.Tags.Select(t =>
                  $"<li>{DisplayTag(t)}</li>"))}
            </ul>
            <label>{series.TitleEncoded}</label>
            <aside>
              <span>{series.EpisodeCount} episodes</span>
              <span>Aired {series.YearRange}</span>
            </aside>
          </div>
        </section>");
  }

  sw.Write("</div>");
}

private void GenerateSeriesDetailsFragment(Series series) {
  var path = Path.Combine(OUTPUT_DIR, "series", $"{series.Slug}.html");
  using var outStream = File.Open(path, FileMode.Create);
  using var sw = new StreamWriter(outStream);
  sw.Write(@$"
    <div class='seriesDetails'>
      <section>
        <img alt='cover image' title='{series.TitleEncoded}'
            src='/cover/sm/{series.SlugEncoded}.jpg'>
        <div>
          <ul>
            {string.Join(string.Empty, series.Tags.Select(t =>
                $"<li>{DisplayTag(t)}</li>"))}
          </ul>
          <label>{series.TitleEncoded}</label>
          <aside>
            <span>{series.EpisodeCount} episodes</span>
            <span>Aired {series.YearRange}</span>
          </aside>
        </div>
      </section>
      <ol>");

  foreach (var episode in GetEpisodes(series.Slug)) {
    sw.Write(
        @$"<li title='{episode.TitleEncoded}'
              data-cover='/cover/sm/{series.SlugEncoded}.jpg'
              data-series='{series.TitleEncoded}'
              data-file='{episode.FileNameEncoded}'>
          <label>{episode.TitleEncoded}</label>
          <aside>
            <span>{episode.LengthDisplay}</span>
            <span>{episode.AirDateDisplay}</span>
          </aside>
        </li>");
  }

  sw.Write("</ol></div>");
}

private void InjectGenreList(IEnumerable<string> genres) {
  var indexPath = Path.Combine(BASE_PATH, "site", "index.html");
  var index = File.ReadAllLines(indexPath);
  using var outStream = File.Create(indexPath);
  using var sw = new StreamWriter(outStream);
  var inGenres = false;
  foreach (var line in index) {
    if (inGenres) {
      if (line.Contains("<!-- end genre list -->")) {
        sw.WriteLine(line);
        inGenres = false;
      }
    } else {
      sw.WriteLine(line);
      if (line.Contains("<!-- begin genre list -->")) {
        foreach (var genre in genres) {
          sw.WriteLine(
              @$"              <li hx-get='/partial/genre/{genre}.html'
                  hx-push-url='/genre/{genre}'
                  hx-target='main'
                  hx-swap='innerHTML show:top'>{DisplayTag(genre)}</li>");
        }

        inGenres = true;
      }
    }
  }
}

private void BuildSite() {
  Batteries.Init(); // initialize sqlite lib

  // generate main series list
  var seriesList = GetSeries();
  GenerateSeriesListFragment(
      seriesList, Path.Combine(OUTPUT_DIR, "series.html"), "All Series");

  // inject genre list
  var tags = GetTags();
  InjectGenreList(tags);

  // generate genre series lists
  foreach (var tag in tags) {
    var path = Path.Combine(OUTPUT_DIR, "genre", $"{tag}.html");
    GenerateSeriesListFragment(seriesList.Where(s => s.Tags.Any(t => t == tag))
        .OrderBy(s => s.TitleSort), path, $"{DisplayTag(tag)} Series");
  }

  // generate series details
  foreach (var series in seriesList) {
    GenerateSeriesDetailsFragment(series);
  }
}

BuildSite();
