#!/usr/bin/env ruby -w

# An HTTP interface for my Harp blog.

require 'json'
require 'sinatra'
require './auth'
require './harp_blog'

CONFIG_DEFAULTS = {
  auth: false,
  dry_run: false,
  path: File.expand_path('../test-blog', __FILE__),
  host: '127.0.0.1',
  hostname: `hostname --fqdn`.strip,
  port: 6706,
  external_port: 6706,
  external_ssl: false,
  preview_port: 9000,
  preview_ssl: false,
}

def env_value(name)
  env_name = "BLOG_#{name.to_s.upcase}"
  raw_value = ENV[env_name]
  case name
  when :auth, :dry_run, :preview_ssl, :external_ssl
    raw_value ? raw_value.to_i != 0 : false
  when :port, :external_port, :preview_port
    raw_value ? raw_value.to_i : nil
  else
    raw_value
  end
end

$config = CONFIG_DEFAULTS.dup
$config.each_key do |name|
  value = env_value(name)
  unless value.nil?
    $config[name] = value
  end
end

if $config[:external_ssl] && $config[:external_port] == 80
  $config[:external_port] = 443
end
if !$config[:external_ssl] && $config[:external_port] == 443
  $config[:external_ssl] = true
end

proto = $config[:preview_ssl] ? 'https' : 'http'
$config[:preview_url] = "#{proto}://#{$config[:hostname]}:#{$config[:preview_port]}"

unless File.directory?($config[:path])
  raise RuntimeError.new("file not found: #{$config[:path]}")
end

if $config[:host] == '0.0.0.0' && !$config[:auth]
  raise RuntimeError.new("cowardly refusing to bind to 0.0.0.0 without authentication")
end

$auth = Auth.new(File.expand_path('../auth.json', __FILE__))
def authenticated?(auth)
  if $config[:auth]
    username, password = auth.to_s.split('|')
    $auth.authenticated?(username, password)
  else
    true
  end
end

host = $config[:hostname] || $config[:host]
proto = $config[:external_ssl] ? 'https' : 'http'
$url_root = "#{proto}://#{host}:#{$config[:external_port]}/"
puts "URL root: #{$url_root}"
def url_for(*components)
  File.join($url_root, *components)
end

# Server

set :host, $config[:host]
set :port, $config[:port]

blog = HarpBlog.new($config[:path], $config[:dry_run])

before do
  if request.body.size > 0
    @fields =
      case
      when request.accept?('application/json')
        request.body.rewind
        json = request.body.read
        JSON.parse(json)
      else
        params
      end
  else
    @fields = {}
  end
end

# favicon
get '/favicon.ico' do
  status 302
  headers 'Location' => "#{$config[:preview_url]}/favicon.ico"
end

# status
get '/status' do
  status 200
  headers 'Content-Type' => 'application/json'
  JSON.generate(status: blog.status)
end

# publish the site
post '/publish' do
  unless authenticated?(request.env['HTTP_AUTH'])
    status 403
    return 'forbidden'
  end

  blog.publish(@fields['env'])
  status 204
end

# sync with github
post '/sync' do
  unless authenticated?(request.env['HTTP_AUTH'])
    status 403
    return 'forbidden'
  end

  blog.sync
  status 204
  nil
end

# list years
get '/years' do
  status 200
  headers 'Content-Type' => 'application/json'
  JSON.generate(years: blog.years)
end

# list months
get '/months' do
  status 200
  headers 'Content-Type' => 'application/json'
  JSON.generate(months: blog.months)
end


##############
### Drafts ###
##############

# list drafts
get '/posts/drafts' do
  posts = blog.drafts

  status 200
  headers 'Content-Type' => 'application/json'
  JSON.generate(posts: posts.map(&:fields))
end

# get a draft
get '/posts/drafts/:id' do |id|
  begin
    post = blog.get_draft(id)
  rescue HarpBlog::InvalidDataError => e
    status 500
    return "Failed to get draft, invalid data on disk: #{e.message}"
  end

  if post
    if request.accept?('text/html')
      status 302
      headers 'Location' => "#{$config[:preview_url]}/posts/drafts/#{id}"
      nil
    elsif request.accept?('application/json')
      status 200
      headers 'Content-Type' => 'application/json'
      JSON.generate(post: post.fields)
    else
      status 400
      "content not available in an acceptable format: #{request.accept.join(', ')}"
    end
  else
    status 404
    'not found'
  end
end

# make a draft, and optionally publish it immediately
post '/posts/drafts' do
  unless authenticated?(request.env['HTTP_AUTH'])
    status 403
    return 'forbidden'
  end

  id, title, body, link = @fields.values_at('id', 'title', 'body', 'link')
  begin
    if post = blog.create_post(title, body, link, id: id, draft: true)
      if env = @fields['env']
        post = blog.publish_post(post)
        Thread.new do
          blog.publish(env)
        end
      end
      url = url_for(post.url)
      status 201
      headers 'Location' => url, 'Content-Type' => 'application/json'
      JSON.generate(post: post.fields)
    else
      status 500
      'failed to create post'
    end
  rescue HarpBlog::PostExistsError => e
    post = HarpBlog::Post.new({
      title: title,
      body: body,
      link: link,
    })
    status 409
    "refusing to clobber existing draft, update it instead: #{post.url}"
  rescue HarpBlog::PostSaveError => e
    status 500
    if orig_err = e.original_error
      "#{e.message} -- #{orig_err.class}: #{orig_err.message}"
    else
      "Failed to create draft: #{e.message}"
    end
  end
end

# update a draft
put '/posts/drafts/:id' do |id|
  unless authenticated?(request.env['HTTP_AUTH'])
    status 403
    return 'forbidden'
  end

  title, body, link = @fields.values_at('title', 'body', 'link')
  begin
    if post = blog.get_draft(id)
      blog.update_post(post, title, body, link, Time.now.to_i)
      status 204
    else
      status 404
      'not found'
    end
  rescue HarpBlog::InvalidDataError => e
    status 500
    "Failed to update draft, invalid data on disk: #{e.message}"
  rescue HarpBlog::PostSaveError => e
    status 500
    if orig_err = e.original_error
      "#{e.message} -- #{orig_err.class}: #{orig_err.message}"
    else
      "Failed to update draft: #{e.message}"
    end
  end
end

# delete a draft
delete '/posts/drafts/:id' do |id|
  unless authenticated?(request.env['HTTP_AUTH'])
    status 403
    return 'forbidden'
  end

  blog.delete_draft(id)
  status 204
end

# publish a post
post '/posts/drafts/:id/publish' do |id|
  unless authenticated?(request.env['HTTP_AUTH'])
    status 403
    return 'forbidden'
  end

  if post = blog.get_draft(id)
    new_post = blog.publish_post(post)
    status 201
    headers 'Location' => url_for(new_post.url), 'Content-Type' => 'application/json'
    JSON.generate(post: new_post.fields)
  else
    status 404
    'not found'
  end
end


#######################
### Published Posts ###
#######################

# list published posts
get '/posts/:year/?:month?' do |year, month|
  posts =
    if month
      blog.posts_for_month(year, month)
    else
      blog.posts_for_year(year)
    end

  status 200
  headers 'Content-Type' => 'application/json'
  JSON.generate(posts: posts.map(&:fields))
end

# list all published posts
get '/posts' do
  posts = blog.months.map do |year, month|
    blog.posts_for_month(year, month)
  end.flatten.reverse

  status 200
  headers 'Content-Type' => 'application/json'
  JSON.generate(posts: posts.map(&:fields))
end

# get a post
get '/posts/:year/:month/:id' do |year, month, id|
  begin
    post = blog.get_post(year, month, id)
  rescue HarpBlog::InvalidDataError => e
    status 500
    return "Failed to get post, invalid data on disk: #{e.message}"
  end

  if post
    if request.accept?('text/html')
      status 302
      headers 'Location'=> "#{$config[:preview_url]}/posts/#{year}/#{month}/#{id}"
      nil
    elsif request.accept?('application/json')
      status 200
      headers 'Content-Type' => 'application/json'
      JSON.generate(post: post.fields)
    else
      status 400
      "content not available in an acceptable format: #{request.accept.join(', ')}"
    end
  else
    status 404
    'not found'
  end
end

# update a post
put '/posts/:year/:month/:id' do |year, month, id|
  unless authenticated?(request.env['HTTP_AUTH'])
    status 403
    return 'forbidden'
  end

  title, body, link = @fields.values_at('title', 'body', 'link')
  begin
    if post = blog.get_post(year, month, id)
      blog.update_post(post, title, body, link)
      status 204
    else
      status 404
      'not found'
    end
  rescue HarpBlog::InvalidDataError => e
    status 500
    "Failed to update post, invalid data on disk: #{e.message}"
  rescue HarpBlog::PostSaveError => e
    status 500
    if orig_err = e.original_error
      "#{e.message} -- #{orig_err.class}: #{orig_err.message}"
    else
      "Failed to update post: #{e.message}"
    end
  end
end

# delete a post
delete '/posts/:year/:month/:id' do |year, month, id|
  unless authenticated?(request.env['HTTP_AUTH'])
    status 403
    return 'forbidden'
  end

  blog.delete_post(year, month, id)
  status 204
end

# unpublish a post
post '/posts/:year/:month/:id/unpublish' do |year, month, id|
  unless authenticated?(request.env['HTTP_AUTH'])
    status 403
    return 'forbidden'
  end

  if post = blog.get_post(year, month, id)
    new_post = blog.unpublish_post(post)
    status 200
    headers 'Location' => url_for(new_post.url), 'Content-Type' => 'application/json'
    JSON.generate(post: new_post.fields)
  else
    status 404
    'not found'
  end
end
