require 'base64'
require 'uri'
require 'net/http'
require './environment.rb'

use Rack::Session::Cookie, key:          'neocities',
                           path:         '/',
                           expire_after: 31556926, # one year in seconds
                           secret:       $config['session_secret']

use Rack::Recaptcha, public_key: $config['recaptcha_public_key'], private_key: $config['recaptcha_private_key']
helpers Rack::Recaptcha::Helpers

helpers do
  def site_change_file_display_class(filename)
    return 'html' if filename.match(Site::HTML_REGEX)
    return 'image' if filename.match(Site::IMAGE_REGEX)
    'misc'
  end

  def csrf_token_input_html
    %{<input name="csrf_token" type="hidden" value="#{csrf_token}">}
  end
end

before do
  if request.path.match /^\/api\//i
    @api = true
    content_type :json
  elsif request.path.match /^\/stripe_webhook$/
    # Skips the CSRF check for stripe web hooks
  else
    content_type :html, 'charset' => 'utf-8'
    redirect '/' if request.post? && !csrf_safe?
  end
end

not_found do
  erb :'not_found'
end

error do
  EmailWorker.perform_async({
    from: 'web@neocities.org',
    to: 'errors@neocities.org',
    subject: "[Neocities Error] #{env['sinatra.error'].class}: #{env['sinatra.error'].message}",
    body: "#{request.request_method} #{request.path}\n\n" +
          (current_site ? "Site: #{current_site.username}\nEmail: #{current_site.email}\n\n" : '') +
          env['sinatra.error'].backtrace.join("\n")
  })

  if @api
    api_error 500, 'server_error', 'there has been an unknown server error, please try again later'
  end

  erb :'error'
end

# :nocov:
get '/home_mockup' do
  erb :'home_mockup'
end

get '/edit_mockup' do
  erb :'edit_mockup'
end

get '/profile_mockup' do
  require_login
  erb :'profile_mockup', locals: {site: current_site}
end

get '/browse_mockup' do
  erb :'browse_mockup'
end

get '/tips_mockup' do
  erb :'tips_mockup'
end
# :nocov:

get '/site/:username.rss' do |username|
  site = Site[username: username]
  content_type :xml
  site.to_rss.to_xml
end

get '/site/:username/?' do |username|
  site = Site[username: username]
  not_found if site.nil? || site.is_banned

  @title = site.title

  @current_page = params[:current_page]
  @current_page = @current_page.to_i
  @current_page = 1 if @current_page == 0

  if params[:event_id]
    event = Event.select(:id).where(id: params[:event_id]).first
    not_found if event.nil?
    events_dataset = Event.where(id: params[:event_id]).paginate(1, 1)
  else
    events_dataset = site.latest_events(@current_page, 10)
  end

  @page_count = events_dataset.page_count || 1
  @latest_events = events_dataset.all

  erb :'site', locals: {site: site, is_current_site: site == current_site}
end

post '/site/:username/set_editor_theme' do
  require_login
  current_site.update editor_theme: params[:editor_theme]
  'ok'
end

post '/settings/create_child' do
  require_login
  site = Site.new

  site.parent_site_id = parent_site.id
  site.username = params[:username]

  if site.valid?
    site.save
    flash[:success] = 'Your new site has been created!'
    redirect '/settings#sites'
  else
    flash[:error] = site.errors.first.last.first
    redirect '/settings#sites'
  end
end

post '/site/:username/comment' do |username|
  require_login

  site = Site[username: username]

  if(site.profile_comments_enabled == false ||
     params[:message].empty? ||
     site.is_blocking?(current_site) ||
     current_site.is_blocking?(site) ||
     current_site.commenting_allowed? == false)
    redirect "/site/#{username}"
  end

  site.add_profile_comment(
    actioning_site_id: current_site.id,
    message: params[:message]
  )

  redirect "/site/#{username}"
end

get '/stats/?' do
  require_admin

  @stats = {
    total_sites: Site.count,
    total_unbanned_sites: Site.where(is_banned: false).count,
    total_banned_sites: Site.where(is_banned: true).count,
    total_nsfw_sites: Site.where(is_nsfw: true).count,
    total_unbanned_nsfw_sites: Site.where(is_banned: false, is_nsfw: true).count,
    total_banned_nsfw_sites: Site.where(is_banned: true, is_nsfw: true).count
  }

  # Start with the date of the first created site

  start = Site.select(:created_at).
               exclude(created_at: nil).
               order(:created_at).
               first[:created_at].to_date

  runner = start

  monthly_stats = []

  now = Time.now

  until runner.year == now.year && runner.month == now.month+1
    monthly_stats.push(
      date: runner,
      sites_created: Site.where(created_at: runner..runner.next_month).count,
      total_from_start: Site.where(created_at: start..runner.next_month).count,
      supporters: Site.where(created_at: start..runner.next_month).exclude(stripe_customer_id: nil).count,
    )

    runner = runner.next_month
  end

  @stats[:monthly_stats] = monthly_stats

  customers = Stripe::Customer.all

  @stats[:total_recurring_revenue] = 0.0

  subscriptions = []
  cancelled_subscriptions = 0

  customers.each do |customer|
    sub = {created_at: Time.at(customer.created)}
    if customer[:subscriptions]
      if customer[:subscriptions][:data].empty?
        sub[:status] = 'cancelled'
      else
        sub[:status] = 'active'
        sub[:amount] = (customer[:subscriptions][:data].first[:plan][:amount] / 100.0).round(2)
        @stats[:total_recurring_revenue] += sub[:amount]
      end
    end
    subscriptions.push sub
  end

  @stats[:subscriptions] = subscriptions
  erb :'stats'
end

get '/?' do
  if current_site
    require_login

    @suggestions = current_site.suggestions

    @current_page = params[:current_page].to_i
    @current_page = 1 if @current_page == 0

    if params[:activity] == 'mine'
      events_dataset = current_site.latest_events(@current_page, 10)
    elsif params[:event_id]
      event = Event.select(:id).where(id: params[:event_id]).first
      not_found if event.nil?
      events_dataset = Event.where(id: params[:event_id]).paginate(1, 1)
    else
      events_dataset = current_site.news_feed(@current_page, 10)
    end

    @page_count = events_dataset.page_count || 1
    @events = events_dataset.all

    current_site.events_dataset.update notification_seen: true

    halt erb :'home', locals: {site: current_site}
  end

  if SimpleCache.expired?(:sites_count)
    @sites_count = SimpleCache.store :sites_count, Site.count.roundup(100), 600 # 10 Minutes
  else
    @sites_count = SimpleCache.get :sites_count
  end

  erb :index, layout: false
end

get '/plan/?' do
  @title = 'Supporter'
  erb :'plan/index'
end

post '/plan/create' do
  require_login

  DB.transaction do
    customer = Stripe::Customer.create(
      card: params[:stripe_token],
      description: "#{parent_site.username} - #{parent_site.id}",
      email: current_site.email,
      plan: params[:selected_plan]
    )

    parent_site.update stripe_customer_id: customer.id, plan_ended: false

    plan_name = customer.subscriptions.first['plan']['name']

    if current_site.email || parent_site.email
      EmailWorker.perform_async({
        from: 'web@neocities.org',
        reply_to: 'contact@neocities.org',
        to: current_site.email || parent_site.email,
        subject: "[Neocities] You've become a supporter!",
        body: Tilt.new('./views/templates/email_subscription.erb', pretty: true).render(self, plan_name: plan_name)
      })
    end
  end

  redirect '/plan'
end

def get_plan_name(customer_id)
  subscriptions = Stripe::Customer.retrieve(parent_site.stripe_customer_id).subscriptions.all
  @plan_name = subscriptions.first.plan.name
end

def require_active_subscription
  redirect '/plan' unless parent_site.supporter? && !parent_site.plan_ended
end

get '/plan/manage' do
  require_login
  require_active_subscription
  @title = 'Manage Plan'
  @plan_name = get_plan_name parent_site.stripe_customer_id
  erb :'plan/manage'
end

get '/plan/end' do
  require_login
  require_active_subscription
  @title = 'End Plan'
  @plan_name = get_plan_name parent_site.stripe_customer_id
  erb :'plan/end'
end

post '/plan/end' do
  require_login
  require_active_subscription

  recaptcha_is_valid = ENV['RACK_ENV'] == 'test' || recaptcha_valid?

  if !recaptcha_is_valid
    @error = 'Recaptcha was filled out incorrectly, please try re-entering.'
    @plan_name = get_plan_name parent_site.stripe_customer_id
    halt erb :'plan/end'
  end

  customer = Stripe::Customer.retrieve parent_site.stripe_customer_id
  subscriptions = customer.subscriptions.all

  DB.transaction do
    subscriptions.each do |subscription|
      customer.subscriptions.retrieve(subscription.id).delete
    end
    parent_site.update plan_ended: true
  end

  redirect '/plan'
end

get '/site/:username/tip' do |username|
  @site = Site[username: username]
  @title = "Tip #{@site.title}"
  erb :'tip'
end

post '/site/:site_id/toggle_follow' do |site_id|
  require_login
  content_type :json
  site = Site[id: site_id]
  {result: (current_site.toggle_follow(site) ? 'followed' : 'unfollowed')}.to_json
end

post '/tags/add' do
  require_login
  current_site.new_tags_string = params[:tags]

  if current_site.valid?
    current_site.save_tags
  else
    flash[:errors] = current_site.errors.first
  end

  redirect request.referer
end

post '/tags/remove' do
  require_login

  DB.transaction {
    params[:tags].each {|tag| current_site.remove_tag Tag[name: tag]}
  }

  redirect request.referer
end

get '/tags/autocomplete/:name.json' do |name|
  Tag.autocomplete(name).collect {|t| t[:name]}.to_json
end

def browse_sites_dataset
  @current_page = params[:current_page]
  @current_page = @current_page.to_i
  @current_page = 1 if @current_page == 0

  site_dataset = Site.filter(is_banned: false, is_crashing: false).filter(site_changed: true)

  if current_site
    if !current_site.blocking_site_ids.empty?
      site_dataset.where!(Sequel.~(Sequel.qualify(:sites, :id) => current_site.blocking_site_ids))
    end

    if current_site.blocks_dataset.count
      site_dataset.where!(
        Sequel.~(Sequel.qualify(:sites, :id) => current_site.blocks_dataset.select(:actioning_site_id).all.collect {|s| s.actioning_site_id})
      )
    end
  end

  case params[:sort_by]
    when 'hits'
      site_dataset.order!(:hits.desc, :updated_at.desc)
    when 'views'
      site_dataset.order!(:views.desc, :updated_at.desc)
    when 'newest'
      site_dataset.order!(:created_at.desc, :views.desc)
    when 'oldest'
      site_dataset.order!(:created_at, :views.desc)
    when 'random'
      site_dataset.where! 'random() < 0.01'
    when 'last_updated'
      params[:sort_by] = 'last_updated'
      site_dataset.order!(:updated_at.desc, :views.desc)
    else
      if params[:tag]
        params[:sort_by] = 'views'
        site_dataset.order!(:views.desc, :updated_at.desc)
      else
        params[:sort_by] = 'last_updated'
        site_dataset.order!(:updated_at.desc, :views.desc)
      end
  end

  site_dataset.where! ['sites.is_nsfw = ?', (params[:is_nsfw] == 'true' ? true : false)]

  if params[:tag]
    site_dataset = site_dataset.association_join(:tags).select_all(:sites)
    site_dataset.where! ['tags.name = ?', params[:tag]]
    site_dataset.where! ['tags.is_nsfw = ?', (params[:is_nsfw] == 'true' ? true : false)]
  end

  site_dataset
end

get '/browse/?' do
  params.delete 'tag' if params[:tag].nil? || params[:tag].empty?
  site_dataset = browse_sites_dataset
  site_dataset = site_dataset.paginate @current_page, 300
  @page_count = site_dataset.page_count || 1
  @sites = site_dataset.all
  erb :browse
end

get '/surf/?' do
  params.delete 'tag' if params[:tag].nil? || params[:tag].empty?
  site_dataset = browse_sites_dataset
  site_dataset = site_dataset.paginate @current_page, 1
  @page_count = site_dataset.page_count || 1
  @site = site_dataset.first
  redirect "/browse?#{Rack::Utils.build_query params}" if @site.nil?
  erb :'surf', layout: false
end

get '/surf/:username' do |username|
  @site = Site.select(:id, :username, :title, :domain, :views).where(username: username).first
  @title = @site.title
  not_found if @site.nil?
  erb :'surf', layout: false
end

get '/api' do
  @title = 'Developers API'
  erb :'api'
end

get '/tutorials' do
  erb :'tutorials'
end

get '/donate' do
  erb :'donate'
end

get '/blog' do
  expires 500, :public, :must_revalidate
  return Net::HTTP.get_response(URI('http://blog.neocities.org')).body
end

get '/blog/:article' do |article|
  expires 500, :public, :must_revalidate
  return Net::HTTP.get_response(URI("http://blog.neocities.org/#{article}.html")).body
end

get '/new' do
  dashboard_if_signed_in
  require_unbanned_ip
  @site = Site.new
  @site.username = params[:username] unless params[:username].nil?
  erb :'new'
end

post '/create_validate' do
  content_type :json

  if !params[:field].match /username|password|email|new_tags_string/
    return {error: 'not a valid field'}.to_json
  end

  site = Site.new(params[:field] => params[:value])
  site.valid?

  field_sym = params[:field].to_sym

  if site.errors[field_sym]
    return {error: site.errors[field_sym].first}.to_json
  end

  {result: 'ok'}.to_json
end

post '/create' do
  content_type :json
  require_unbanned_ip
  dashboard_if_signed_in

  @site = Site.new(
    username: params[:username],
    password: params[:password],
    email: params[:email],
    new_tags_string: params[:tags],
    ip: request.ip
  )

  if !@site.valid?
    return {result: 'error'}.to_json
  end

  @site.save

  EmailWorker.perform_async({
    from: 'web@neocities.org',
    reply_to: 'contact@neocities.org',
    to: @site.email,
    subject: "[Neocities] Welcome to Neocities!",
    body: Tilt.new('./views/templates/email_welcome.erb', pretty: true).render(self)
  })

  send_confirmation_email @site

  session[:id] = @site.id
  {result: 'ok'}.to_json
end

get '/dashboard' do
  require_login

  if params[:dir] && params[:dir][0] != '/'
    params[:dir] = '/'+params[:dir]
  end

  if !File.directory?(current_site.files_path(params[:dir]))
    redirect '/dashboard'
  end

  @dir = params[:dir]
  @file_list = current_site.file_list @dir
  erb :'dashboard'
end

get '/settings/?' do
  require_login
  @site = parent_site
  erb :'settings/account'
end

def require_ownership_for_settings
  @site = Site[username: params[:username]]

  not_found if @site.nil?

  unless @site.owned_by? parent_site
    flash[:error] = 'Cannot edit this site, you do not have permission.'
    redirect request.referrer
  end
end

get '/settings/:username/?' do
  require_login
  require_ownership_for_settings
  erb :'settings/site'
end

post '/settings/:username/profile' do
  require_login
  require_ownership_for_settings

  @site.update(
    profile_comments_enabled: params[:site][:profile_comments_enabled]
  )
  flash[:success] = 'Profile settings changed.'
  redirect "/settings/#{@site.username}#profile"
end

post '/settings/:username/ssl' do
  require_login
  require_ownership_for_settings

  unless params[:key] && params[:cert]
    flash[:error] = 'SSL key and certificate are required.'
    redirect "/settings/#{@site.username}#custom_domain"
  end

  begin
    key = OpenSSL::PKey::RSA.new params[:key][:tempfile].read, ''
  rescue => e
    flash[:error] = 'Could not process SSL key, file may be incorrect, damaged, or passworded (you need to remove the password).'
    redirect "/settings/#{@site.username}#custom_domain"
  end

  if !key.private?
    flash[:error] = 'SSL Key file does not have private key data.'
    redirect "/settings/#{@site.username}#custom_domain"
  end

  certs_string = params[:cert][:tempfile].read

  cert_array = certs_string.lines.slice_before(/-----BEGIN CERTIFICATE-----/).to_a.collect {|a| a.join}

  if cert_array.empty?
    flash[:error] = 'Cert file does not contain any certificates.'
    redirect "/settings/#{@site.username}#custom_domain"
  end

  cert_valid_for_domain = false

  cert_array.each do |cert_string|
    begin
      cert = OpenSSL::X509::Certificate.new cert_string
    rescue => e
      flash[:error] = 'Could not process SSL certificate, file may be incorrect or damaged.'
      redirect "/settings/#{@site.username}#custom_domain"
    end

    if cert.not_after < Time.now
      flash[:error] = 'SSL Certificate has expired, please create a new one.'
      redirect "/settings/#{@site.username}#custom_domain"
    end

    cert_cn = cert.subject.to_a.select {|a| a.first == 'CN'}.flatten[1]
    cert_valid_for_domain = true if cert_cn && cert_cn.match(@site.domain)
  end

  unless cert_valid_for_domain
    flash[:error] = "Your certificate CN (common name) does not match your domain: #{@site.domain}"
    redirect "/settings/#{@site.username}#custom_domain"
  end

  # Everything else was worse.

  crtfile = Tempfile.new 'crtfile'
  crtfile.write cert_array.join
  crtfile.close

  keyfile = Tempfile.new 'keyfile'
  keyfile.write key.to_pem
  keyfile.close

  if ENV['TRAVIS'] != 'true'
    nginx_testfile = Tempfile.new 'nginx_testfile'
    nginx_testfile.write %{
      pid /tmp/throwaway.pid;
      events {}
      error_log /dev/null error;
      http {
        access_log off;
        server {
          listen 60000 ssl;
          server_name #{@site.domain} *.#{@site.domain};
          ssl_certificate #{crtfile.path};
          ssl_certificate_key #{keyfile.path};
        }
      }
    }
    nginx_testfile.close

    line = Cocaine::CommandLine.new(
      "nginx", "-t -c :path",
      expected_outcodes: [0],
      swallow_stderr: true
    )

    begin
      output = line.run path: nginx_testfile.path
    rescue Cocaine::ExitStatusError => e
      flash[:error] = "There is something wrong with your certificate, please check with your issuing CA."
      redirect "/settings/#{@site.username}#custom_domain"
    end
  end

  @site.update ssl_key: key.to_pem, ssl_cert: cert_array.join

  flash[:success] = 'Updated SSL key/certificate.'
  redirect "/settings/#{@site.username}#custom_domain"
end

post '/settings/:username/change_name' do
  require_login
  require_ownership_for_settings

  old_username = @site.username

  if params[:name] == nil || params[:name] == ''
    flash[:error] = 'Name cannot be blank.'
    redirect "/settings/#{@site.username}#username"
  end

  if old_username == params[:name]
    flash[:error] = 'You already have this name.'
    redirect "/settings/#{@site.username}#username"
  end

  old_host = @site.host
  old_file_paths = @site.file_list.collect {|f| f[:path]}

  @site.username = params[:name]

  if @site.valid?
    DB.transaction {
      @site.save_changes
      @site.move_files_from old_username
    }

    old_file_paths.each do |file_path|
      @site.purge_cache file_path
    end

    flash[:success] = "Site/user name has been changed. You will need to use this name to login, <b>don't forget it</b>."
    redirect "/settings/#{@site.username}#username"
  else
    flash[:error] = @site.errors.first.last.first
    redirect "/settings/#{old_username}#username"
  end
end

post '/settings/:username/change_nsfw' do
  require_login
  require_ownership_for_settings

  @site.update is_nsfw: params[:is_nsfw]
  flash[:success] = @site.is_nsfw ? 'Marked 18+' : 'Unmarked 18+'
  redirect "/settings/#{@site.username}#nsfw"
end

post '/settings/:username/custom_domain' do
  require_login
  require_ownership_for_settings

  @site.domain = params[:domain]

  if @site.valid?
    @site.save_changes
    flash[:success] = 'The domain has been successfully updated.'
    redirect "/settings/#{@site.username}#custom_domain"
  else
    flash[:error] = @site.errors.first.last.first
    redirect "/settings/#{@site.username}#custom_domain"
  end
end

post '/settings/change_password' do
  require_login

  if !Site.valid_login?(parent_site.username, params[:current_password])
    flash[:error] = 'Your provided password does not match the current one.'
    redirect "/settings#password"
  end

  parent_site.password = params[:new_password]
  parent_site.valid?

  if params[:new_password] != params[:new_password_confirm]
    parent_site.errors.add :password, 'New passwords do not match.'
  end

  if parent_site.errors.empty?
    parent_site.save_changes
    flash[:success] = 'Successfully changed password.'
    redirect "/settings#password"
  else
    flash[:error] = current_site.errors.first.last.first
    redirect '/settings#password'
  end
end

post '/settings/change_email' do
  require_login
  
  if params[:email] == parent_site.email
    flash[:error] = 'You are already using this email address for this account.'
    redirect '/settings#email'
  end

  parent_site.email = params[:email]
  parent_site.email_confirmation_token = SecureRandom.hex 3
  parent_site.email_confirmed = false

  if parent_site.valid?
    parent_site.save_changes
    send_confirmation_email
    flash[:success] = 'Successfully changed email. We have sent a confirmation email, please use it to confirm your email address.'
    redirect '/settings#email'
  end

  flash[:error] = parent_site.errors.first.last.first
  redirect '/settings#email'
end

get '/password_reset' do
  erb :'password_reset'
end

post '/send_password_reset' do
  sites = Site.filter(email: params[:email]).all

  if sites.length > 0
    token = SecureRandom.uuid.gsub('-', '')
    sites.each do |site|
      site.update password_reset_token: token
    end

    body = <<-EOT
Hello! This is the Neocities cat, and I have received a password reset request for your e-mail address. Purrrr.

Go to this URL to reset your password: http://neocities.org/password_reset_confirm?token=#{token}

After clicking on this link, your password for all the sites registered to this email address will be changed to this token.

Token: #{token}

If you didn't request this reset, you can ignore it. Or hide under a bed. Or take a nap. Your call.

Meow,
the Neocities Cat
    EOT

    body.strip!

    EmailWorker.perform_async({
      from: 'web@neocities.org',
      to: params[:email],
      subject: '[Neocities] Password Reset',
      body: body
    })
  end

  flash[:success] = 'If your email was valid (and used by a site), the Neocities Cat will send an e-mail to your account with password reset instructions.'
  redirect '/'
end

get '/password_reset_confirm' do
  if params[:token].nil? || params[:token].empty?
    flash[:error] = 'Could not find a site with this token.'
    redirect '/'
  end

  reset_site = Site[password_reset_token: params[:token]]

  if reset_site.nil?
    flash[:error] = 'Could not find a site with this token.'
    redirect '/'
  end

  sites = Site.filter(email: reset_site.email).all

  if sites.length > 0
    sites.each do |site|
      site.password = reset_site.password_reset_token
      site.save_changes
    end

    flash[:success] = 'Your password for all sites with your email address has been changed to the token sent in your e-mail. Please login and change your password as soon as possible.'
  else
    flash[:error] = 'Could not find a site with this token.'
  end

  redirect '/'
end

get '/signin/?' do
  dashboard_if_signed_in
  erb :'signin'
end

post '/signin' do
  dashboard_if_signed_in

  if Site.valid_login? params[:username], params[:password]
    site = Site.get_with_identifier params[:username]

    if site.is_banned
      flash[:error] = 'Invalid login.'
      flash[:username] = params[:username]
      redirect '/signin'
    end

    session[:id] = site.id
    redirect '/'
  else
    flash[:error] = 'Invalid login.'
    flash[:username] = params[:username]
    redirect '/signin'
  end
end

get '/signout' do
  require_login
  session[:id] = nil
  redirect '/'
end

get '/signin/:username' do
  require_login
  @site = Site[username: params[:username]]

  not_found if @site.nil?

  if @site.owned_by? current_site
    session[:id] = @site.id
    redirect request.referrer
  end

  flash[:error] = 'You do not have permission to switch to this site.'
  redirect request.referrer
end

get '/about' do
  erb :'about'
end

get '/site_files/new_page' do
  require_login
  erb :'site_files/new_page'
end

post '/site_files/create_page' do
  require_login
  @errors = []

  params[:pagefilename].gsub!(/[^a-zA-Z0-9_\-.]/, '')
  params[:pagefilename].gsub!(/\.html$/i, '')

  if params[:pagefilename].nil? || params[:pagefilename].empty?
    @errors << 'You must provide a file name.'
    halt erb(:'site_files/new_page')
  end

  name = "#{params[:pagefilename]}.html"

  if current_site.file_exists?(name)
    @errors << %{Web page "#{name}" already exists! Choose another name.}
    halt erb(:'site_files/new_page')
  end

  current_site.install_new_html_file name

  flash[:success] = %{#{name} was created! <a style="color: #FFFFFF; text-decoration: underline" href="/site_files/text_editor/#{name}">Click here to edit it</a>.}

  redirect '/dashboard'
end

get '/site_files/new' do
  require_login
  erb :'site_files/new'
end

def file_upload_response(error=nil)
  http_error_code = 406

  if params[:from_button]
    if error
      @error = error
      halt 200, erb(:'dashboard')
    else
      query_string = params[:dir] ? "?"+Rack::Utils.build_query(dir: params[:dir]) : ''
      redirect "/dashboard#{query_string}"
    end
  else
    halt http_error_code, error if error
    halt 200, 'File(s) successfully uploaded.'
  end
end

post '/site/create_directory' do
  require_login

  path = "#{params[:dir] || ''}/#{params[:name]}"

  result = current_site.create_directory path

  if result != true
    flash[:error] = e.message
  end

  redirect "/dashboard?dir=#{Rack::Utils.escape params[:dir]}"
end

post '/site_files/upload' do
  require_login
  @errors = []
  http_error_code = 406

  if params[:files].nil?
    file_upload_response "Uploaded files were not seen by the server, cancelled. We don't know what's causing this yet. Please contact us so we can help fix it. Thanks!"
  end

  params[:files].each do |file|
    file[:filename] = "#{params[:dir]}/#{file[:filename]}" if params[:dir]
    if current_site.file_size_too_large? file[:tempfile].size
      file_upload_response "#{params[:dir]}/#{file[:filename]} is too large, upload cancelled."
    end
    if !Site.valid_file_type? file
      file_upload_response "#{params[:dir]}/#{file[:filename]}: file type (or content in file) is not allowed on Neocities, upload cancelled."
    end
  end

  uploaded_size = params[:files].collect {|f| f[:tempfile].size}.inject{|sum,x| sum + x }

  if current_site.file_size_too_large? uploaded_size
    file_upload_response "File(s) do not fit in your available space, upload cancelled."
  end

  results = []
  params[:files].each do |file|
    results << current_site.store_file(file[:filename], file[:tempfile])
  end
  current_site.increment_changed_count if results.include?(true)

  file_upload_response
end

post '/site_files/delete' do
  require_login
  current_site.delete_file params[:filename]

  flash[:success] = "Deleted #{params[:filename]}."
  redirect '/dashboard'
end

get '/site_files/:username.zip' do |username|
  require_login
  zipfile_path = current_site.files_zip
  content_type 'application/octet-stream'
  attachment   "neocities-#{current_site.username}.zip"
  send_file zipfile_path
end

get '/site_files/download/:filename' do |filename|
  require_login
  content_type 'application/octet-stream'
  attachment filename
  current_site.get_file filename
end

get %r{\/site_files\/text_editor\/(.+)} do
  require_login
  @filename = params[:captures].first
  begin
    @file_data = current_site.get_file @filename
  rescue Errno::ENOENT
    flash[:error] = 'We could not find the requested file.'
    redirect '/dashboard'
  end
  erb :'site_files/text_editor'
end

post %r{\/site_files\/save\/(.+)} do
  require_login_ajax
  filename = params[:captures].first

  tempfile = Tempfile.new 'neocities_saving_file'

  input = request.body.read
  tempfile.set_encoding input.encoding
  tempfile.write input
  tempfile.close

  if current_site.file_size_too_large? tempfile.size
    halt 'File is too large to fit in your space, it has NOT been saved. You will need to reduce the size or upgrade to a new plan.'
  end

  current_site.store_file filename, tempfile

  'ok'
end

get '/site_files/allowed_types' do
  erb :'site_files/allowed_types'
end

get '/site_files/mount_info' do
  erb :'site_files/mount_info'
end

get '/terms' do
  erb :'terms'
end

get '/privacy' do
  erb :'privacy'
end

get '/admin' do
  require_admin
  @banned_sites = Site.select(:username).filter(is_banned: true).order(:username).all
  @nsfw_sites = Site.select(:username).filter(is_nsfw: true).order(:username).all
  erb :'admin'
end

post '/admin/banip' do
  require_admin
  site = Site[username: params[:username]]

  if site.nil?
    flash[:error] = 'User not found'
    redirect '/admin'
  end

  if site.ip.nil? || site.ip.empty?
    flash[:error] = 'IP is blank, cannot continue'
    redirect '/admin'
  end

  sites = Site.filter(ip: site.ip, is_banned: false).all
  sites.each {|s| s.ban!}
  flash[:error] = "#{sites.length} sites have been banned."
  redirect '/admin'
end

post '/admin/banhammer' do
  require_admin

  site = Site[username: params[:username]]

  if site.nil?
    flash[:error] = 'User not found'
    redirect '/admin'
  end

  if site.is_banned
    flash[:error] = 'User is already banned'
    redirect '/admin'
  end

  site.ban!

  flash[:success] = 'MISSION ACCOMPLISHED'
  redirect '/admin'
end

post '/admin/mark_nsfw' do
  require_admin
  site = Site[username: params[:username]]

  if site.nil?
    flash[:error] = 'User not found'
    redirect '/admin'
  end

  site.is_nsfw = true
  site.save_changes validate: false

  flash[:success] = 'MISSION ACCOMPLISHED'
  redirect '/admin'
end

get '/contact' do
  erb :'contact'
end

post '/contact' do

  @errors = []

  if params[:email].empty? || params[:subject].empty? || params[:body].empty?
    @errors << 'Please fill out all fields'
  end

  if !recaptcha_valid?
    @errors << 'Captcha was not filled out (or was filled out incorrectly)'
  end

  if !@errors.empty?
    erb :'contact'
  else
    EmailWorker.perform_async({
      from: 'web@neocities.org',
      reply_to: params[:email],
      to: 'contact@neocities.org',
      subject: "[Neocities Contact]: #{params[:subject]}",
      body: params[:body]
    })

    flash[:success] = 'Your contact has been sent.'
    redirect '/'
  end
end

post '/stripe_webhook' do
  event = JSON.parse request.body.read
  if event['type'] == 'customer.created'
    username  = event['data']['object']['description']
    email     = event['data']['object']['email']
  end
  'ok'
end

post '/api/upload' do
  require_api_credentials
  files = []
  params.each do |k,v|
    next unless v.is_a?(Hash) && v[:tempfile]
    path = k.to_s
    files << {filename: k || v[:filename], tempfile: v[:tempfile]}
  end

  api_error 400, 'missing_files', 'you must provide files to upload' if files.empty?

  uploaded_size = files.collect {|f| f[:tempfile].size}.inject{|sum,x| sum + x }

  if current_site.file_size_too_large? uploaded_size
    api_error 400, 'too_large', 'files are too large to fit in your space, try uploading smaller (or less) files'
  end

  files.each do |file|
    if !Site.valid_file_type?(file)
      api_error 400, 'invalid_file_type', "#{file[:filename]} is not a valid file type (or contains not allowed content), files have not been uploaded"
    end

    if File.directory? file[:filename]
      api_error 400, 'directory_exists', 'this name is being used by a directory, cannot continue'
    end
  end

  results = []
  files.each do |file|
    results << current_site.store_file(file[:filename], file[:tempfile])
  end

  current_site.increment_changed_count if results.include?(true)

  api_success 'your file(s) have been successfully uploaded'
end

post '/api/delete' do
  require_api_credentials

  api_error 400, 'missing_filenames', 'you must provide files to delete' if params[:filenames].nil? || params[:filenames].empty?

  paths = []
  params[:filenames].each do |path|
    unless path.is_a?(String)
      api_error 400, 'bad_filename', "#{path} is not a valid filename, canceled deleting"
    end

    if !current_site.file_exists?(path)
      api_error 400, 'missing_files', "#{path} was not found on your site, canceled deleting"
    end

    if path == 'index.html'
      api_error 400, 'cannot_delete_index', 'you cannot delete your index.html file, canceled deleting'
    end

    paths << path
  end

  paths.each do |path|
    current_site.delete_file(path)
  end

  api_success 'file(s) have been deleted'
end

get '/api/info' do
  if params[:sitename]
    site = Site[username: params[:sitename]]

    api_error 400, 'site_not_found', "could not find site #{params[:sitename]}" if site.nil? || site.is_banned
    api_success api_info_for(site)
  else
    init_api_credentials
    api_success api_info_for(current_site)
  end
end

def api_info_for(site)
  {
    info: {
      sitename: site.username,
      views: site.views,
      hits: site.hits,
      created_at: site.created_at.rfc2822,
      last_updated: site.updated_at.rfc2822,
      domain: site.domain,
      tags: site.tags.collect {|t| t.name}
    }
  }
end

# Catch-all for missing api calls

get '/api/:name' do
  api_not_found
end

post '/api/:name' do
  api_not_found
end

post '/event/:event_id/toggle_like' do |event_id|
  require_login
  content_type :json
  event = Event[id: event_id]
  liked_response = event.toggle_site_like(current_site) ? 'liked' : 'unliked'
  {result: liked_response, event_like_count: event.likes_dataset.count, liking_site_names: event.liking_site_usernames}.to_json
end

post '/event/:event_id/comment' do |event_id|
  require_login
  content_type :json
  event = Event[id: event_id]

  site = event.site

  if site.is_blocking?(current_site) ||
     site.profile_comments_enabled == false ||
     current_site.commenting_allowed? == false
    return {result: 'error'}.to_json
  end

  event.add_site_comment current_site, params[:message]
  {result: 'success'}.to_json
end

post '/event/:event_id/update_profile_comment' do |event_id|
  require_login
  content_type :json
  event = Event[id: event_id]
  return {result: 'error'}.to_json unless current_site.id == event.profile_comment.actioning_site_id

  event.profile_comment.update message: params[:message]
  return {result: 'success'}.to_json
end

post '/event/:event_id/delete' do |event_id|
  require_login
  content_type :json
  event = Event[id: event_id]

  if event.site_id == current_site.id || event.created_by?(current_site)
    event.delete
    return {result: 'success'}.to_json
  end

  return {result: 'error'}.to_json
end

post '/comment/:comment_id/toggle_like' do |comment_id|
  require_login
  content_type :json
  comment = Comment[id: comment_id]
  liked_response = comment.toggle_site_like(current_site) ? 'liked' : 'unliked'
  {result: liked_response, comment_like_count: comment.comment_likes_dataset.count, liking_site_names: comment.liking_site_usernames}.to_json
end

post '/comment/:comment_id/delete' do |comment_id|
  require_login
  content_type :json
  comment = Comment[id: comment_id]

  if comment.event.site == current_site || comment.actioning_site == current_site
    comment.delete
    return {result: 'success'}.to_json
  end

  return {result: 'error'}.to_json
end

get '/site/:username/confirm_email/:token' do
  site = Site[username: params[:username]]
  if site.email_confirmation_token == params[:token]
    site.email_confirmed = true
    site.save_changes

    erb :'site_email_confirmed'
  else
    erb :'site_email_not_confirmed'
  end
end

post '/site/:username/report' do |username|
  site = Site[username: username]

  redirect request.referer if site.nil?

  report = Report.new site_id: site.id, type: params[:type], comments: params[:comments]

  if current_site
    redirect request.referer if current_site.id == site.id
    report.reporting_site_id = current_site.id
  else
    report.ip = request.ip
  end

  report.save

  EmailWorker.perform_async({
    from: 'web@neocities.org',
    to: 'report@neocities.org',
    subject: "[Neocities Report] #{site.username} has been reported for #{report.type}",
    body: "Reported by #{report.reporting_site_id ? report.reporting_site.username : report.ip}: #{report.comments}"
  })

  redirect request.referer
end

post '/site/:username/block' do |username|
  require_login
  site = Site[username: username]
  redirect request.referer if current_site.id == site.id

  current_site.block! site

  if request.referer.match /\/site\/#{username}/i
    redirect '/'
  else
    redirect request.referer
  end
end

def require_admin
  redirect '/' unless signed_in? && current_site.is_admin
end

def dashboard_if_signed_in
  redirect '/dashboard' if signed_in?
end

def require_login_ajax
  halt 'You are banned.' if Site.banned_ip?(request.ip)
  halt 'You are not logged in!' unless signed_in?
end

def csrf_safe?
  csrf_token == params[:csrf_token] || csrf_token == request.env['HTTP_X_CSRF_TOKEN']
end

def csrf_token
   session[:_csrf_token] ||= SecureRandom.base64(32)
end

def require_login
  require_unbanned_ip
  redirect '/' unless signed_in?
end

def signed_in?
  !session[:id].nil?
end

def current_site
  return nil if session[:id].nil?
  @_site ||= Site[id: session[:id]]
end

def parent_site
  return nil if current_site.nil?
  current_site.parent? ? current_site : current_site.parent
end

def require_unbanned_ip
  if Site.banned_ip?(request.ip)
    session[:id] = nil
    flash[:error] = 'Your IP address has been banned due to misconduct. '+
    'If you believe this to be in error, <a href="/contact">contact the site admin</a>.'
    redirect '/'
  end
end

def title
  out = "Neocities"
  return out                  if request.path == '/'
  return "#{out} - #{@title}" if @title
  "#{out} - #{request.path.gsub('/', '').capitalize}"
end

def encoding_fix(file)
  begin
    Rack::Utils.escape_html file
  rescue ArgumentError => e
    return Rack::Utils.escape_html(file.force_encoding('BINARY')) if e.message =~ /invalid byte sequence in UTF-8/
    fail
  end
end

def require_api_credentials
  if !request.env['HTTP_AUTHORIZATION'].nil?
    init_api_credentials
  else
    api_error_invalid_auth
  end
end

def init_api_credentials
  auth = request.env['HTTP_AUTHORIZATION']

  begin
    user, pass = Base64.decode64(auth.match(/Basic (.+)/)[1]).split(':')
  rescue
    api_error_invalid_auth
  end

  if Site.valid_login? user, pass
    site = Site[username: user]

    if site.nil? || site.is_banned
      api_error_invalid_auth
    end

    session[:id] = site.id
  else
    api_error_invalid_auth
  end
end

def api_success(message_or_obj)
  output = {result: 'success'}

  if message_or_obj.is_a?(String)
    output[:message] = message_or_obj
  else
    output.merge! message_or_obj
  end

  api_response(200, output)
end

def api_response(status, output)
  halt status, JSON.pretty_generate(output)+"\n"
end

def api_error(status, error_type, message)
  api_response(status, result: 'error', error_type: error_type, message: message)
end

def api_error_invalid_auth
  api_error 403, 'invalid_auth', 'invalid credentials - please check your username and password'
end

def api_not_found
  api_error 404, 'not_found', 'the requested api call does not exist'
end

def send_confirmation_email(site=current_site)
  EmailWorker.perform_async({
    from: 'web@neocities.org',
    reply_to: 'contact@neocities.org',
    to: site.email,
    subject: "[Neocities] Confirm your email address",
    body: Tilt.new('./views/templates/email_confirm.erb', pretty: true).render(self, site: site)
  })
end