#!/usr/bin/env ruby

require "fileutils"
require "pathname"
require "cgi"
require "yaml"

ROOT = Pathname.new(__dir__).join("../..").expand_path
OUTPUT_DIR = ROOT.join("mdbook/src")
SUMMARY_CONFIG_PATH = ROOT.join("mdbook/summary.yml")
MEDIA_EXTENSIONS = %w[.svg .png .jpg .jpeg .gif .webp .zip].freeze

def source_markdown_files
  Dir.glob(ROOT.join("**/*.md").to_s).sort.filter_map do |path|
    pathname = Pathname.new(path)
    next if pathname.to_s.include?("/.git/")
    next if pathname.to_s.include?("/ruby-examples/")
    next if pathname.to_s.include?("/mdbook/")
    next if pathname.basename.to_s.end_with?(".excalidraw.md")
    next if pathname.size.zero?

    pathname
  end
end

def media_files
  MEDIA_EXTENSIONS.flat_map do |extension|
    Dir.glob(ROOT.join("**/*#{extension}").to_s)
  end.sort.filter_map do |path|
    pathname = Pathname.new(path)
    next if pathname.to_s.include?("/.git/")
    next if pathname.to_s.include?("/mdbook/")

    pathname
  end
end

def output_relative_for_source(pathname)
  relative = pathname.relative_path_from(ROOT)
  return Pathname.new("README.md") if relative.to_s == "Introduction.md"

  relative
end

def title_for(pathname)
  pathname.basename(".md").to_s
end

def page_title_for(pathname)
  relative = pathname.relative_path_from(ROOT).to_s
  return "Introduction" if relative == "Introduction.md"

  title_for(pathname)
end

def load_summary_config
  YAML.load_file(SUMMARY_CONFIG_PATH)
end

def summary_entries(config)
  entries = [{ "file" => "README.md", "title" => "Introduction", "part" => nil }]

  Array(config["parts"]).each do |part|
    part_title = part["title"]
    Array(part["chapters"]).each do |chapter|
      entries << chapter.merge("part" => part_title)
    end
  end

  entries
end

def build_source_lookup(files)
  exact = {}
  by_title = Hash.new { |hash, key| hash[key] = [] }

  files.each do |file|
    relative = file.relative_path_from(ROOT)
    exact[relative.to_s.downcase] = file
    exact[title_for(file).downcase] = file
    by_title[title_for(file).downcase] << file
  end

  [exact, by_title]
end

def svg_index
  @svg_index ||= begin
    index = Hash.new { |hash, key| hash[key] = [] }

    Dir.glob(ROOT.join("**/*.svg").to_s).sort.each do |path|
      pathname = Pathname.new(path)
      next if pathname.to_s.include?("/.git/")
      next if pathname.to_s.include?("/mdbook/")

      index[pathname.basename.to_s.downcase] << pathname
    end

    index
  end
end

def resolve_svg_for_embed(markdown_file, raw_target)
  target = raw_target.strip
  target_svg = target.sub(/\.excalidraw\z/i, ".svg")
  target_excalidraw_svg = "#{target}.svg"

  [target_excalidraw_svg, target_svg].each do |candidate_name|
    explicit_candidate = ROOT.join(candidate_name)
    return explicit_candidate if explicit_candidate.exist?

    relative_candidate = markdown_file.dirname.join(candidate_name).expand_path
    return relative_candidate if relative_candidate.exist?
  end

  [target_excalidraw_svg, target_svg].each do |candidate_name|
    matches = svg_index[Pathname.new(candidate_name).basename.to_s.downcase]
    return matches.first if matches.length == 1
  end

  nil
end

def resolve_markdown_target(current_file, raw_target, exact_lookup, title_lookup)
  target = raw_target.strip
  note_target, anchor = target.split("#", 2)
  note_target = note_target.strip

  return [current_file, anchor] if note_target.empty?

  candidates = []
  if note_target.end_with?(".md")
    candidates << current_file.dirname.join(note_target).expand_path
    candidates << ROOT.join(note_target)
  else
    candidates << current_file.dirname.join("#{note_target}.md").expand_path
    candidates << ROOT.join("#{note_target}.md")
  end

  candidates.each do |candidate|
    return [candidate, anchor] if candidate.exist?
  end

  downcased = note_target.downcase
  return [exact_lookup[downcased], anchor] if exact_lookup.key?(downcased)
  return [exact_lookup["#{downcased}.md"], anchor] if exact_lookup.key?("#{downcased}.md")
  return [exact_lookup["#{downcased}s"], anchor] if exact_lookup.key?("#{downcased}s")
  return [exact_lookup["#{downcased}es"], anchor] if exact_lookup.key?("#{downcased}es")

  title_matches = title_lookup[downcased]
  return [title_matches.first, anchor] if title_matches.length == 1

  plural_matches = title_lookup["#{downcased}s"]
  return [plural_matches.first, anchor] if plural_matches.length == 1

  es_matches = title_lookup["#{downcased}es"]
  return [es_matches.first, anchor] if es_matches.length == 1

  [nil, anchor]
end

def slugify_anchor(anchor)
  anchor.downcase.strip.gsub(/[^\p{Alnum}\s-]/, "").gsub(/\s+/, "-")
end

def encode_href(path)
  path.to_s.split("/").map { |part| CGI.escape(part).gsub("+", "%20") }.join("/")
end

def rewrite_excalidraw_embeds(markdown_file, content, warnings)
  output_file = OUTPUT_DIR.join(output_relative_for_source(markdown_file))

  content.gsub(/!\[\[([^\]|]+?\.excalidraw)(\|[^\]]+)?\]\]/i) do
    raw_target = Regexp.last_match(1)
    svg_source = resolve_svg_for_embed(markdown_file, raw_target)

    unless svg_source
      warnings << "Missing SVG export for #{raw_target} referenced in #{markdown_file.relative_path_from(ROOT)}"
      next Regexp.last_match(0)
    end

    svg_output = OUTPUT_DIR.join(svg_source.relative_path_from(ROOT))
    relative_link = svg_output.relative_path_from(output_file.dirname)
    "![](#{encode_href(relative_link)})"
  end
end

def transform_non_code_fences(content)
  segments = content.split(/^```.*$\n?/)
  fences = content.scan(/^```.*$\n?/)
  transformed = +""

  segments.each_with_index do |segment, index|
    if index.even?
      transformed << yield(segment)
    else
      transformed << segment
    end

    transformed << fences[index].to_s
  end

  transformed
end

def rewrite_wikilinks(markdown_file, content, exact_lookup, title_lookup, warnings)
  output_file = OUTPUT_DIR.join(output_relative_for_source(markdown_file))

  transform_non_code_fences(content) do |segment|
    segment.gsub(/(?<!!)\[\[([^\]]+)\]\]/) do
      body = Regexp.last_match(1)
      target, alias_text = body.split("|", 2)
      display_text = alias_text&.strip
      resolved, anchor = resolve_markdown_target(markdown_file, target, exact_lookup, title_lookup)

      unless resolved
        warnings << "Unresolved wikilink #{body} in #{markdown_file.relative_path_from(ROOT)}"
        next display_text || target
      end

      relative_target = OUTPUT_DIR.join(output_relative_for_source(resolved)).relative_path_from(output_file.dirname)
      href = encode_href(relative_target)
      href = "#{href}##{slugify_anchor(anchor)}" if anchor && !anchor.empty?
      label = display_text || target.split("#", 2).first.strip
      "[#{label}](#{href})"
    end
  end
end

def generate_summary(existing_relatives, config)
  lines = ["# Summary", "", "[Introduction](README.md)", ""]
  started = { nil => true }

  summary_entries(config).each do |entry|
    relative = entry.fetch("file")
    next unless existing_relatives.include?(relative)

    part = entry["part"]
    if part && !started[part]
      lines << "# #{part}"
      lines << ""
      started[part] = true
    end

    next if relative == "README.md"

    title = entry["title"] || Pathname.new(relative).basename(".md").to_s
    encoded_relative = encode_href(relative)
    lines << "- [#{title}](#{encoded_relative})"
  end

  lines << ""
  lines.join("\n")
end

files = source_markdown_files
exact_lookup, title_lookup = build_source_lookup(files)
summary_config = load_summary_config

FileUtils.rm_rf(OUTPUT_DIR)
FileUtils.mkdir_p(OUTPUT_DIR)

warnings = []
existing_relatives = []

files.each do |markdown_file|
  output_relative = output_relative_for_source(markdown_file)
  output_file = OUTPUT_DIR.join(output_relative)
  FileUtils.mkdir_p(output_file.dirname)

  content = markdown_file.read
  transformed = rewrite_excalidraw_embeds(markdown_file, content, warnings)
  transformed = rewrite_wikilinks(markdown_file, transformed, exact_lookup, title_lookup, warnings)
  transformed = "# #{page_title_for(markdown_file)}\n\n#{transformed.lstrip}"

  output_file.write(transformed)
  existing_relatives << output_relative.to_s
end

media_files.each do |media_file|
  output_file = OUTPUT_DIR.join(media_file.relative_path_from(ROOT))
  FileUtils.mkdir_p(output_file.dirname)
  FileUtils.cp(media_file, output_file)
end

OUTPUT_DIR.join("SUMMARY.md").write(generate_summary(existing_relatives, summary_config))

puts "Prepared mdBook source in #{OUTPUT_DIR}"
puts "Markdown files: #{files.length}"
puts "Media files copied: #{media_files.length}"

if warnings.empty?
  puts "All embeds and wikilinks were resolved."
else
  puts "Warnings:"
  warnings.uniq.each { |warning| puts " - #{warning}" }
end
