#### Aya - A Gemini server
#### Copyright (C) 2021 Remilia Scarlet <remilia@posteo.jp>
####
#### This program is free software: you can redistribute it and/or modify
#### it under the terms of the GNU Affero General Public License as
#### published by the Free Software Foundation, either version 3 of the
#### License, or (at your option) any later version.
####
#### This program is distributed in the hope that it will be useful,
#### but WITHOUT ANY WARRANTY; without even the implied warranty of
#### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#### GNU Affero General Public License for more details.
####
#### You should have received a copy of the GNU Affero General Public License
#### along with this program.  If not, see <https://www.gnu.org/licenses/>.
require "yaml"

module Aya
  struct Config
    include YAML::Serializable
    include YAML::Serializable::Strict

    # Dictates how symlinks are handled.
    enum FollowType
      None
      #RootOnly
      All
    end

    # Specifies the rate limiting behavior.
    enum RateLimitBehavior
      All
      NonGemini
    end

    # Where to look for files to serve.
    @[YAML::Field(key: "server-root")]
    getter serverRoot : String

    # The port number to use for connections.
    @[YAML::Field(key: "listen-port")]
    getter listenPort : UInt16 = 1965u16

    # Specifies how symlinks should be handled.
    @[YAML::Field(key: "follow-symlinks")]
    getter followSymlinks : FollowType = FollowType::None

    # If this is not an empty string, this tells Aya to append a footer onto the
    # end of every Gemtext page served.
    getter footer : String = ""

    # Where to find a mime.types file for MIME media type information.  If this
    # is blank (the default), then the default behavior of the `MIME` is used,
    # with handlers for text/gemini added.
    @[YAML::Field(key: "mime-file")]
    getter mimeFile : String = ""

    # The location of the TLS certificate key.
    @[YAML::Field(key: "tls-key")]
    getter tlsKey : String

    # The location of the certificate chain
    @[YAML::Field(key: "cert-chain")]
    getter certChain : String

    # When non-nil, change to this UID after startup.  If this is not specified,
    # it uses the UID of whoever started the program.
    getter uid : Int32?

    # When non-nil, change to this GID after startup.  If this is not specified,
    # it uses the GID of whoever started the program.
    getter gid : Int32?

    # Dictates how files of unknown MIME media types are served.  If this is
    # `true`, files of unknown types are served as application/octet-stream.
    # Otherwise they are not served, error type 40 is sent, and an error is
    # printed on the console.
    @[YAML::Field(key: "unknown-as-octet-stream")]
    getter unknownAsOctetStream : Bool = false

    # When this is a non-empty string, all messages that are logged to standard
    # output/standard error will be logged to this file as well.
    @[YAML::Field(key: "log-file")]
    getter logFile : String = ""

    # When this is a non-empty string, all connections are logged here.
    @[YAML::Field(key: "conn-log-file")]
    getter connLogFile : String = ""

    # When greater than zero, limit transfers to this number of kilobytes per
    # second.
    @[YAML::Field(key: "rate-limit")]
    getter rateLimit : UInt64 = 0u64

    # Controls what gets rate limited.  When this is set to
    # `RateLimitBehavior::All`, all content is rate limited.  If this is
    # `RateLimitBehavior::NonGemini`, then anything that is _not_ of type
    # text/gemini is rate limited.
    @[YAML::Field(key: "rate-limit-type")]
    getter rateLimitType : RateLimitBehavior = RateLimitBehavior::NonGemini

    # Which network addresses to listen on.  There must be at least one.
    getter listen : Array(String) = ["127.0.0.1"]

    # Files with these extensions will be treated as CGI programs to execute.
    # Anything sent to standard output by these programs will be served as a
    # response body.  Output should include the response header and any
    # applicable body.
    @[YAML::Field(key: "cgi-extensions")]
    getter cgiExtensions : Array(String) = [] of String

    # What to use for the PATH environment variable for CGI scripts.  This is
    # passed verbatim to the PATH environment variable when invoking a CGI
    # script.
    @[YAML::Field(key: "cgi-path")]
    getter cgiPath : String = ""

    # Creates a new `Config` by loading one from `path`.  If `path` doesn't
    # exist, or the file cannot be loaded, this will call
    # `RemiLog::Logger#fatal` with an appropriate error message.
    def self.fromFile(path : String|Path)
      begin
        if File.exists?(path)
          @@config = Config.from_yaml(File.open(path))
        else
          RemiLog.log.fatal("Cannot find file: #{path}")
        end
      rescue err : YAML::Error
        RemiLog.log.fatal("#{err}")
      end
    end

    # Performs various checks to see if this is a valid config file.
    # If it is, this returns Nil.  Otherwise it will call
    # `RemiLog::Logger#fatal` with the appropriate message.
    def validate
      unless Dir.exists?(@serverRoot)
        RemiLog.log.fatal("Server root does not exist: #{Aya.config.serverRoot}")
      end

      unless File.exists?(@tlsKey)
        RemiLog.log.fatal("TLS certificate key does not exist: #{Aya.config.tlsKey}")
      end

      unless File.exists?(@certChain)
        RemiLog.log.fatal("TLS certificate chain does not exist: #{Aya.config.certChain}")
      end

      if !@mimeFile.empty? && !File.exists?(@mimeFile)
        RemiLog.log.fatal("MIME types file does not exist: #{Aya.config.mimeFile}")
      end

      if @listen.empty?
        RemiLog.log.fatal("No interfaces to listen on")
      end
    end

    # Prints this configuration file using `RemiLog::Logger#vlog`.
    def report
      RemiLog.log.vlog("Log file: #{Aya.config.logFile}")
      RemiLog.log.vlog("Conn Log file: #{Aya.config.connLogFile}")
      RemiLog.log.vlog("Server root: #{Aya.config.serverRoot}")
      RemiLog.log.vlog("Listen Port: #{Aya.config.listenPort}")
      RemiLog.log.vlog("Symlink behavior: #{Aya.config.followSymlinks}")
      RemiLog.log.vlog("MIME Types File: #{Aya.config.mimeFile}")
      RemiLog.log.vlog("TLS Key: #{Aya.config.tlsKey}")
      RemiLog.log.vlog("Certificate Chain: #{Aya.config.certChain}")
      RemiLog.log.vlog("Send unknown types as application/octet-stream: #{Aya.config.unknownAsOctetStream}")
      RemiLog.log.vlog(String.build do |str|
                         str << "Rate limit: "
                         if Aya.config.rateLimit == 0
                           str << "Disabled"
                         else
                           str << "#{Aya.config.rateLimit} KiB/sec"
                         end
                       end)
      RemiLog.log.vlog(String.build do |str|
                         str << "Interfaces:\n"
                         str << Aya.config.listen.join("\n") do |elt|
                           "#{" " * (RemiLog.log.verboseHeader.size + 6)}* #{elt}"
                         end
                       end)
    end
  end
end
