require 'oj'
require 'pry'
require 'erb'
require 'time'
require 'sinatra/base'
require "sinatra/namespace"
require 'sinatra/cross_origin'
require './lib/config.rb'
require './lib/helpers.rb'

class CraneOp < Sinatra::Base
  register Sinatra::CrossOrigin
  register Sinatra::Namespace

  configure do
    enable :cross_origin
    enable :sessions
    mime_type :javascript, 'application/javascript'
    mime_type :javascript, 'text/javascript'
    set :logging, true
    set :static, true
    set :allow_origin, :any
    set :allow_methods, [:get, :post, :options]
    set :allow_credentials, true
    set :max_age, "1728000"
    set :expose_headers, ['Content-Type']
    set :json_encoder, :to_json
    set :session_secret, Configuration.new.session_secret
  end

  def conf
    return Configuration.new
  end

  ## Basic Auth ##

  if Configuration.new.username
    config = Configuration.new
    use Rack::Auth::Basic, "Please Authenticate to View" do |username, password|
      username == config.username and password == ( config.password || '' )
    end
  end

    ## Registry API Methods ##

  def fetch_catalog(next_string=nil)
    query={n: 100, last: next_string}
    json = get("/v2/_catalog", conf, session, {}, query)
    if json['errors']
      return json
    end
    return [] if json['repositories'].nil?
    repos = []
    repos += json['repositories']
    unless json['repositories'].empty?
      repos += fetch_catalog(repos.last)
    end
    return repos
  end

  def containers(filter=nil)
    repos = fetch_catalog
    return repos if repos.is_a?(Hash) && repos['errors']
    if filter
      return repos.select{ |i| i.match(/#{filter}.*/)}
    end
    return repos
  end

  def container_tags(repo, filter=nil)
    json = get("/v2/#{repo}/tags/list", conf, session)
    return nil if json['tags'].nil?
    tags = json['tags'] || []
    if filter
      return sort_versions(tags.select{ |i| i.match(/#{filter}.*/)}).reverse
    end
    sort_versions(tags).reverse
  end

  def container_info(repo, manifest)
    json = get("/v2/#{repo}/manifests/#{manifest}", conf, session)

    # Add extra fields for easy display
    if json['history']
      history = json['history'].shift
      json['information'] = Oj.load(history['v1Compatibility'])

      json['layer_info'] = []
      json['history'].each do |i|
        json['layer_info'] << Oj.load(i['v1Compatibility'])
      end
      json['layer_info'].reverse!
      created_at = Time.parse(json['information']['created'])
      json['information']['created_formatted'] = created_at.to_s
      json['information']['created_millis']    = (created_at.to_f * 1000).to_i
    end

    return json
  end

  def fetch_digest(repo, manifest)
    response = get_head("/v2/#{repo}/manifests/#{manifest}", conf, session, { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json'})
    return response.headers["docker-content-digest"]
  end

  def image_delete(repo, manifest)
    digest = fetch_digest(repo, manifest)
    return send_delete("/v2/#{repo}/manifests/#{digest}", conf, session, { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json'})
  end

  namespace '/'+ENV['CONTEXT'] do
    include Helpers

    ## Endpoints ##

    get '/api' do
      return "API Version #{conf.version}"
    end

    get '/api/containers' do
      content_type :json
      repos = containers(params[:filter])
      if !repos.is_a?(Array)
        halt 401, {'content-type' => 'application/json'}, {'message' => "Registry requires authentication"}.to_json
      end
      repos.to_json
    end

    get '/api/tags/*' do |container|
      content_type :json
      tags = container_tags(container, params[:filter])
      halt 404 if tags.nil?
      tags.to_json
    end

    post '/api/login' do
      content_type :json
      params = Oj.load(request.body.read)
      session.delete(:username)
      session.delete(:password)
      username = params['username']
      password = params['password']
      if check_login(conf, session, username, password)
        session[:username] = username
        session[:password] = password
        return {status: "success"}.to_json
      end
      halt 401, {error: "credentials are wrong"}.to_json
    end

    get '/logout' do
      session.destroy
      redirect '/'
    end

    get /api\/containers\/(.*\/)(.*)/ do |container, tag|

      # This is here because we need to handle slashes in container names
      container.chop!

      content_type :json

      info = container_info(container, tag)

      halt 404 if info['errors']
      halt 404 if info['fsLayers'].nil?

      info.to_json
    end

    get '/api/registryinfo' do
      content_type :json
      info = {
        host: conf.registry_host,
        public_url: conf.registry_public_url,
        port: conf.registry_port,
        protocol: conf.registry_protocol,
        ssl_verify: conf.ssl_verify,
        delete_allowed: conf.delete_allowed,
        login_allowed: conf.login_allowed,
        title: conf.title,
      }
      if session[:username]
        info[:username] = session[:username]
      end
      info.to_json
    end

    delete /api\/containers\/(.*\/)(.*)/ do |container, tag|
      halt 404 unless to_bool(conf.delete_allowed)

      container.chop!
      response = image_delete( container, tag )
      headers = response.headers
      response.body
    end

    # React app endpoints
    [
      "/containers/*",
      "/containers",
      "/login",
      "/login/",
      "/",
    ].each do |path|
      get path do
        erb :index, locals: { title: conf.title}
      end
    end

    # Error Handlers
    error do
      File.read(File.join('public', '500.html'))
    end

    not_found do
      status 404
      File.read(File.join('public', '404.html'))
    end

    # Debug endpoints
    if Configuration.new.debug
      get '/api/session' do
        session.to_hash.to_json
      end

      get '/api/config' do
        conf.to_hash.to_json
      end

      get '/api/login' do
        content_type :json
        session.delete(:username)
        session.delete(:password)
        username = params['username']
        password = params['password']
        if check_login(conf, session, username, password)
          session[:username] = username
          session[:password] = password
          return {status: "success"}.to_json
        end
        halt 401, {error: "credentials are wrong"}.to_json
      end

    end
  end

end
