# frozen_string_literal: true

require "fixtury/definition"
require "fixtury/path"
require "fixtury/errors/already_defined_error"
require "fixtury/errors/fixture_not_defined_error"
require "fixtury/errors/schema_frozen_error"

module Fixtury
  class Schema

    attr_reader :definitions, :children, :name, :parent, :relative_name, :around_fixture_definition, :options

    def initialize(parent:, name:, options: {})
      @name = name
      @parent = parent
      @relative_name = @name.split("/").last
      @around_fixture_definition = nil
      @options = options
      @frozen = false
      reset!
    end

    def around_fixture(&block)
      @around_fixture_definition = block
    end

    def around_fixture_hook(executor, &definition)
      maybe_invoke_parent_around_fixture_hook(executor) do
        if around_fixture_definition.nil?
          yield
        else
          around_fixture_definition.call(executor, definition)
        end
      end
    end

    def maybe_invoke_parent_around_fixture_hook(executor, &block)
      return yield unless parent

      parent.around_fixture_hook(executor, &block)
    end

    def reset!
      @children = {}
      @definitions = {}
    end

    def freeze!
      @frozen = true
    end

    def frozen?
      !!@frozen
    end

    def top_level_schema
      top_level_schema? ? self : parent.top_level_schema
    end

    def top_level_schema?
      parent.nil?
    end

    def define(&block)
      ensure_not_frozen!
      instance_eval(&block)
      self
    end

    # helpful for inspection
    def structure(indent = "")
      out = []
      out << "#{indent}ns:#{relative_name}"
      definitions.keys.sort.each do |key|
        out << "#{indent}  defn:#{key}"
      end

      children.keys.sort.each do |key|
        child = children[key]
        out << child.structure("#{indent}  ")
      end

      out.join("\n")
    end

    def namespace(name, options = {}, &block)
      ensure_not_frozen!
      ensure_no_conflict!(name: name, definitions: true, namespaces: false)

      child = find_or_create_child_schema(name: name, options: options)
      child.instance_eval(&block) if block_given?
      child
    end

    def fixture(name, options = {}, &block)
      ensure_not_frozen!
      ensure_no_conflict!(name: name, definitions: true, namespaces: true)
      create_child_definition(name: name, options: options, &block)
    end

    def enhance(name, &block)
      ensure_not_frozen!
      definition = get_definition!(name)
      definition.enhance(&block)
      definition
    end

    def merge(other_ns)
      ensure_not_frozen!
      other_ns.definitions.each_pair do |name, dfn|
        fixture(name, &dfn.callable)
        dfn.enhancements.each do |e|
          enhance(name, &e)
        end
      end

      other_ns.children.each_pair do |name, other_ns_child|
        namespace(name) do
          merge(other_ns_child)
        end
      end

      around_fixture(&other_ns.around_fixture_definition) if other_ns.around_fixture_definition

      self
    end

    def get_definition!(name)
      dfn = get_definition(name)
      raise ::Fixtury::Errors::FixtureNotDefinedError, name unless dfn

      dfn
    end

    def get_definition(name)
      path = ::Fixtury::Path.new(namespace: self.name, path: name)
      top_level = top_level_schema

      dfn = nil
      path.possible_absolute_paths.each do |abs_path|
        *namespaces, definition_name = abs_path.split("/")

        namespaces.shift if namespaces.first == top_level.name
        target = top_level

        namespaces.each do |ns|
          next if ns.empty?

          target = target.children[ns]
          break unless target
        end

        dfn = target.definitions[definition_name] if target
        return dfn if dfn
      end

      nil
    end

    def get_namespace(name)
      path = ::Fixtury::Path.new(namespace: self.name, path: name)
      top_level = top_level_schema

      path.possible_absolute_paths.each do |abs_path|
        *namespaces, _definition_name = abs_path.split("/")

        namespaces.shift if namespaces.first == top_level.name
        target = top_level

        namespaces.each do |ns|
          next if ns.empty?

          target = target.children[ns]
          break unless target
        end

        return target if target
      end

      nil
    end

    protected

    def find_child_schema(name:)
      children[name.to_s]
    end

    def find_or_create_child_schema(name:, options:)
      name = name.to_s
      child = find_child_schema(name: name)
      child ||= begin
        children[name] = begin
          child_name = build_child_name(name: name)
          self.class.new(name: child_name, parent: self, options: options)
        end
      end
      child
    end

    def find_child_definition(name:)
      definitions[name.to_s]
    end

    def create_child_definition(name:, options:, &block)
      child_name = build_child_name(name: name)
      definition = ::Fixtury::Definition.new(name: child_name, schema: self, options: options, &block)
      definitions[name.to_s] = definition
    end

    def build_child_name(name:)
      name = name&.to_s
      raise ArgumentError, "`name` must be provided" if name.nil?
      raise ArgumentError, "#{name} is invalid. `name` must contain only a-z, A-Z, 0-9, and _." unless /^[a-zA-Z_0-9]+$/.match?(name)

      arr = ["", self.name, name]
      arr.join("/").gsub(%r{/{2,}}, "/")
    end

    def ensure_no_conflict!(name:, namespaces:, definitions:)
      if definitions
        definition = find_child_definition(name: name)
        raise ::Fixtury::Errors::AlreadyDefinedError, definition.name if definition
      end

      if namespaces
        ns = find_child_schema(name: name)
        raise ::Fixtury::Errors::AlreadyDefinedError, ns.name if ns
      end
    end

    def ensure_not_frozen!
      return unless frozen?

      raise ::Fixtury::Errors::SchemaFrozenError
    end

  end
end