def decode_percent(str)
  result = ""
  i = 0
  while i < str.length
    char = str[i]
    if char == '%' && i + 2 < str.length
      # %の後の2文字を16進数として解釈
      hex = str[i + 1, 2]
      result << hex.to_i(16).chr
      i += 3
    elsif char == '+'
      # URLエンコードではスペースが '+' になることがあるため
      result << ' '
      i += 1
    else
      result << char
      i += 1
    end
  end
  result
end

class IndexComponent < Funicular::Component
  PER = 10

  def initialize_state
    { posts: [], total: 0, page: 1, loading: false, categories: [] }
  end

  use_suspense :index,
    ->(resolve, reject) {
      Funicular::HTTP.get("/api/posts?page=1&per=#{PER}") do |response|
        response.ok ? resolve.call(response.data) : reject.call(response.error_message)
      end
    },
    on_resolve: ->(data) {
      patch(posts: data['posts'], total: data['total'], page: 1)
    }

  use_suspense :categories,
    ->(resolve, reject) {
      Funicular::HTTP.get("/api/categories") do |response|
        response.ok ? resolve.call(response.data) : reject.call(response.error_message)
      end
    },
    on_resolve: ->(data) {
      patch(categories: data['categories'] || [])
    }

  def load_page(page)
    patch(loading: true)
    Funicular::HTTP.get("/api/posts?page=#{page}&per=#{PER}") do |response|
      if response.ok
        patch(posts: response.data['posts'], total: response.data['total'], page: page, loading: false)
      else
        patch(loading: false)
      end
    end
  end

  def render
    total_pages = ((state.total.to_f / PER).ceil).to_i
    total_pages = 1 if total_pages < 1

    div do
      h1 { "NGT Blog" }
      a(href: "https://x.com/pyayy") { "NGT（ながた）" }
      span { "のブログです" }
      h2 { "投稿一覧" }
      suspense(
        fallback: -> { div { "読み込み中..." } },
        error: ->(e) { div { "エラー: #{e}" } }
      ) do
        if state.loading
          div { "読み込み中..." }
        else
          state.posts.each do |post|
            div do
              link_to post_path(post['key']), navigate: true do
                "#{post['published_at']}  #{post['title'] || post['key']}"
              end
            end
          end
        end

        div do
          button(onclick: -> { load_page(state.page - 1) }, disabled: state.page <= 1) { "← 前へ" }
          span { " #{state.page} / #{total_pages} " }
          button(onclick: -> { load_page(state.page + 1) }, disabled: state.page >= total_pages) { "次へ →" }
        end
      end
      unless state.categories.empty?
        h3 { "カテゴリ一覧" }
        div do
          state.categories.each do |cat|
            link_to category_path(cat), navigate: true do
              "[#{cat}]"
            end
            span { " " }
          end
        end
      end
    end
  end
end

class PostComponent < Funicular::Component
  def initialize_state
    {}
  end

  use_suspense :post,
    ->(resolve, reject) {
      Funicular::HTTP.get("/api/post/#{props[:id]}") do |response|
        response.ok ? resolve.call(response.data) : reject.call(response.error_message)
        render_markdown
      end
    }

  def component_updated(prev_state)
    render_markdown
  end

  def render_markdown
    container = refs[:markdown_body]
    return unless container
    content = post['body']
    return unless content
    container.innerHTML = JS.global.marked.parse(content)
  end

  def component_unmounted
    container = refs[:markdown_body]
    container.innerHTML = "" if container
  end

  def render
    div do
      suspense(
        fallback: -> { div { "読み込み中..." } },
        error: ->(e) { div { "エラー: #{e}" } }
      ) do
        h1 { post['title'] }
        p { "投稿日: #{post['published_at']}" }
        p do
          span { "カテゴリ: " }
          link_to category_path(post['category'] || 'カテゴリ未設定'), navigate: true do
            decode_percent(post['category'] || 'カテゴリ未設定')
          end
        end
        div(ref: :markdown_body)
      end
      link_to index_path, navigate: true do
        "一覧に戻る"
      end
    end
  end
end

class CategoryComponent < Funicular::Component
  PER = 10

  def initialize_state
    { posts: [], total: 0, page: 1, loading: false }
  end

  use_suspense :category,
    ->(resolve, reject) {
      Funicular::HTTP.get("/api/posts/category/#{props[:category]}?page=1&per=#{PER}") do |response|
        response.ok ? resolve.call(response.data) : reject.call(response.error_message)
      end
    },
    on_resolve: ->(data) {
      patch(posts: data['posts'], total: data['total'], page: 1)
    }

  def load_page(page)
    patch(loading: true)
    Funicular::HTTP.get("/api/posts/category/#{props[:category]}?page=#{page}&per=#{PER}") do |response|
      if response.ok
        patch(posts: response.data['posts'], total: response.data['total'], page: page, loading: false)
      else
        patch(loading: false)
      end
    end
  end

  def render
    total_pages = ((state.total.to_f / PER).ceil).to_i
    total_pages = 1 if total_pages < 1

    div do
      h1 { "NGT Blog" }
      h2 { "カテゴリ: #{decode_percent(props[:category])}" }
      suspense(
        fallback: -> { div { "読み込み中..." } },
        error: ->(e) { div { "エラー: #{e}" } }
      ) do
        if state.loading
          div { "読み込み中..." }
        else
          state.posts.each do |post|
            div do
              link_to post_path(post['key']), navigate: true do
                "#{post['published_at']}  #{post['title'] || post['key']}"
              end
            end
          end
        end

        div do
          button(onclick: -> { load_page(state.page - 1) }, disabled: state.page <= 1) { "← 前へ" }
          span { " #{state.page} / #{total_pages} " }
          button(onclick: -> { load_page(state.page + 1) }, disabled: state.page >= total_pages) { "次へ →" }
        end
      end
      link_to index_path, navigate: true do
        "トップに戻る"
      end
    end
  end
end

class NotFoundComponent < Funicular::Component
  def render
    div do
      h1 { "ページが見つかりません" }
    end
  end
end

Funicular.start(container: 'app') do |router|
  router.get('/', to: IndexComponent, as: 'index')
  router.get('/post/:id', to: PostComponent, as: 'post')
  router.get('/category/:category', to: CategoryComponent, as: 'category')
end