class MicroSystem::DockerShip < MicroSystem::Ship

  def self.up(log_level: self.log_level, filename: "docker-compose.yml")
    compose("up", log_level:, filename:)
  end

  def self.start(log_level: self.log_level, filename: "docker-compose.yml")
    compose("start", log_level:, filename:)
  end

  def self.stop(log_level: self.log_level, filename: "docker-compose.yml")
    compose("stop", log_level:, filename:)
  end

  def self.restart(log_level: self.log_level, filename: "docker-compose.yml")
    compose("restart", log_level:, filename:)
  end

  def self.compose(action, log_level: self.log_level, filename: "docker-compose.yml")
    dock(log_level:, filename:)

    sh "docker compose -f #{filename} #{action}"
  end

  def self.dock(log_level: self.log_level, filename: "docker-compose.yml")
    log_level log_level
    menv = {log_level:}
    comments = get_comments
    content = get_content(menv)
    content = comments + content

    puts stick system.color, content if log? :higher
    FileShell.write_text filename, content, log_level: :highest
  end

  def self.get_content(menv)
    ship_menv = {log_level: menv[:log_level]}
    self.call(ship_menv)
    services = ship_menv[:services].map { [_1.to_s, _2.result] }.to_h
    content = {"services" => services}

    Object.send :require, "yaml"
    YAML.dump content
  end

  def self.call(menv)
    super

    menv[:services] = {}
    used_services.each do |name, used_service|
      menv[:services][name] = used_service
      used_service.process
    end
  end

  def self.get_comments
    content = FileShell.read_text self.source_location_path, log_level: :higher
    Object.send :require, "digest/md5"
    md5 = Digest::MD5.hexdigest content
    path = self.source_location_path.sub App.root.to_s, ""
    <<~YAML
      # DO NOT EDIT THIS FILE MANUALLY - THIS FILE WAS AUTO-GENERATED
      #
      # This docker-compose.yml was generated from service definitions in:
      #   #{path} (#{self})
      #
      # MD5: #{md5}
      #
      # To regenerate this file, run:
      #   liza ship:compose
      #
      # To run the services defined here:
      #   docker compose up
      #
      # To run and stop services using detached mode:
      #   docker compose up -d
      #   docker compose down
    YAML
  end

  section :services

  def self.defined_services
    fetch(:defined_services) { {} }
  end

  def self.used_services
    @used_services ||= {}
  end

  class Service
    def initialize(name, supername)
      @name = name
      @supername = supername
    end

    def blocks()= @blocks ||= []
    def result()= @result ||= {}

    def add_block(block)
      blocks << block if block
    end

    attr_accessor :name, :supername, :ship

    def process
      process_ship
      process_blocks_shifting
      process_blocks
      process_persisted_volume
      process_default_port
    end

    def process_ship
      self.ship = Liza.const(:"#{ship}_ship") if ship.is_a?(Symbol)
    end

    def process_blocks_shifting
      service = ship.defined_services[supername]
      raise "service #{name} not defined in #{ship}" if service.nil?
      @blocks = service.blocks + blocks
    end

    def process_blocks
      blocks.each do |block|
        self.instance_eval(&block)
      end
    end

    def process_persisted_volume
      volumes "./data/#{App.mode}/micro_ships/#{name}:#{persisted}" if persisted
    end

    def process_default_port
      port default_port unless result.key?("ports")
    end

    # properties

    def image(string)
      result["image"] = string
    end

    def command(string)
      result["command"] = string
    end

    def working_dir(string)
      result["working_dir"] = string
    end

    def default_port(number=nil)
      if number
        @default_port = number
      else
        @default_port
      end
    end

    def port(host_port, container_port=default_port)
      raise "not default port" unless default_port
      ports "#{host_port}:#{container_port}"
    end

    def ports(string)
      raise "not default port" unless default_port
      raise "must include ':'" unless string.include? ":"

      result["ports"] ||= []
      result["ports"] << string
    end

    def persisted(path = nil, mode: nil)
      if path
        @persisted = path
        @persisted += ":#{mode}" if mode
      else
        @persisted
      end
    end

    def volumes(string)
      raise "must include ':'" unless string.include? ":"
      result["volumes"] ||= []
      result["volumes"] << string
    end

    def environment(hash)
      result["environment"] ||= {}
      result["environment"].merge!(hash.map {|k, v| [k.to_s, v.to_s] }.to_h)
    end

    def depends_on(*services)
      raise "At least one dependency required" if services.empty?
      unless services.all? { |s| s.is_a?(Symbol) || s.is_a?(String) }
        raise "Dependencies must be symbols or strings"
      end
      result["depends_on"] = services.map(&:to_s)
    end

    def restart(value)
      allowed = 
      {
        no:             "no",
        always:         "always",
        on_failure:     "on-failure",
        unless_stopped: "unless-stopped"
      }

      key = value.to_sym
      raise "#{value.inspect} not in #{allowed.keys.inspect}" unless allowed.key?(key)

      result["restart"] = allowed[key]
    end

    def set_value(key, value)
      result[key.to_s] = value
    end

    def add_value(key, value)
      result[key.to_s] ||= []
      result[key.to_s] << value
    end

  end

  def self.define_service(name, &block)
    log :highest, "defining #{name.inspect}"
    defined_services[name] ||= Service.new(name, name)
    defined_services[name].add_block block
  end

  def self.use_service(supername, name=supername, ship: self, &block)
    defined_service = defined_services[supername]

    used_service = used_services[name] ||= Service.new(name, supername)
    used_service.ship = ship
    used_service.add_block block

    log :highest, "using #{name.inspect}"
  end

end
