#!/usr/bin/env elixir

defmodule TimestampRestorer do
  @environment System.get_env("MIX_ENV", "dev")
  @db_path "_build/#{@environment}/timestamp-database"

  def sha_all(opts \\ []) do
    timestamp_database = load_timestamp_database()

    "**/*.{ex,exs,beam}"
    |> Path.wildcard()
    |> Enum.reduce(%{}, fn filename, acc ->
      {sha, timestamp} = process(filename, timestamp_database, opts)
      Map.put(acc, sha, timestamp)
    end)
    |> write_timestamp_database()
  end

  defp load_timestamp_database() do
    if File.exists?(@db_path) do
      @db_path
      |> File.read!()
      |> String.split("\n")
      |> Enum.reduce(%{}, fn line, acc ->
        [sha, timestamp_string] = String.split(line, ":")
        {timestamp, ""} = Integer.parse(timestamp_string)
        Map.put(acc, sha, timestamp)
      end)
    else
      %{}
    end
  end

  defp write_timestamp_database(database) do
    File.mkdir_p!(Path.dirname(@db_path))

    database
    |> Enum.map(fn {key, value} -> "#{key}:#{value}" end)
    |> Enum.join("\n")
    |> (& File.write!(@db_path, &1)).()
  end

  defp process(filename, timestamp_database, opts) do
    {verbose, _opts} = Keyword.pop(opts, :verbose, false)
    sha = sha(filename)
    {:ok, %{mtime: new_timestamp}} = File.lstat(filename, time: :posix)

    case Map.get(timestamp_database, sha) do
      nil ->
        log("[NEW SHA  ] #{filename}: #{new_timestamp}", verbose)
      timestamp when timestamp < new_timestamp ->
        log("[RESTORED ] #{filename}: #{timestamp}", verbose)
        File.touch(filename, timestamp)
      timestamp when timestamp >= new_timestamp ->
        log("[UNCHANGED] #{filename}: #{timestamp}", verbose)
    end

    {sha, new_timestamp}
  end

  defp log(_message, false), do: :ok
  defp log(message, true), do: :logger.debug(message)

  defp sha(filename) do
    hash_ref = :crypto.hash_init(:sha)

    File.stream!(filename)
    |> Enum.reduce(hash_ref, fn chunk, prev_ref->
      new_ref = :crypto.hash_update(prev_ref, chunk)
      new_ref
    end)
    |> :crypto.hash_final()
    |> Base.encode16()
    |> String.downcase()
  end
end

{opts, _args, _errors} = OptionParser.parse(System.argv(), switches: [verbose: :boolean])

{time, _result} = :timer.tc(TimestampRestorer, :sha_all, [opts])

:logger.info("Restored timestamps in #{time / 1_000_000}s")
