# frozen_string_literal: true

require_dependency 'scribo/application_record'

module Scribo
  # Represents any content in the system
  class Content < ApplicationRecord
    has_closure_tree
    belongs_to :site, class_name: 'Site', foreign_key: 'scribo_site_id'
    has_one_attached :asset

    validate :post_path
    validate :layout_cant_be_current_content

    before_save :store_properties, if: :config?
    before_save :upload_asset

    after_save -> { store_full_path(true) }
    after_update -> {store_full_path(true)}

    scope :layouts, -> { in_folder('_layouts') }
    scope :posts, -> { in_folder('_posts') }
    scope :pages, -> { not_in_folder('_posts').restricted.where(kind: 'text') }
    scope :assets, -> { where(kind: 'asset') }
    scope :html_pages, -> { where("full_path LIKE '%.html' OR full_path LIKE '%.md' OR full_path LIKE '%.markdown'") }
    # html files should be non-filtered html files
    scope :html_files, -> { where("full_path LIKE '%.html'") }
    scope :include, ->(name) { published.where(full_path: ["/_includes/#{name}"]) }
    scope :layout, lambda { |name|
                     published.where(full_path: %W[/_layouts/#{name}.html /_layouts/#{name}.md /_layouts/#{name}.xml /_layouts/#{name}.css])
                   }
    scope :data, lambda { |name|
                   published.where(full_path: %W[/_data/#{name}.yml /_data/#{name}.yaml /_data/#{name}.json /_data/#{name}.csv /_data/#{name}])
                 }

    scope :locale, ->(name) { published.where(full_path: "/_locales/#{name}.yml") }
    scope :locales, -> { published.in_folder('_locales') }

    scope :published, lambda {
                        where("properties->>'published' = 'true' OR properties->>'published' IS NULL").where("properties->>'published_at' IS NULL OR properties->>'published_at' <= :now", now: Time.current.utc)
                      }
    scope :restricted, -> { where("full_path NOT LIKE '/\\_%'") }

    scope :not_in_folder, lambda { |folder_name|
                            where.not(id: in_folder(folder_name).pluck(:id))
                          }
    scope :in_folder, lambda { |folder_name|
                        where(kind: 'folder').find_by(path: folder_name)&.descendants || none
                      }

    scope :permalinked, ->(paths) { where("properties->>'permalink' IN (?)", paths) }

    def self.located(path, restricted: true)
      restricted = true if restricted.nil? # If blank it's still restricted

      result = published.where(full_path: search_paths_for(path))
      result = result.restricted if restricted
      result.or(published.permalinked(search_paths_for(path)))
    end

    def path
      self[:path]
    end

    # Uses https://www.postgresql.org/docs/current/textsearch-controls.html
    def self.search(search_string)
      where(
        "to_tsvector(scribo_contents.data || ' ' || COALESCE(scribo_contents.properties::text, '')) @@ to_tsquery(?)", search_string
      )
    end

    # Name of the currently in use layout
    def layout_name
      properties&.key?('layout') ? properties&.[]('layout') : ''
    end

    # Layout as content
    def layout
      return nil unless layout_name.present?

      site.contents.layout(layout_name).first
    end

    def identifier
      File.basename(path, File.extname(path))
    end

    # Data with frontmatter, used for maintenance and import/export
    def data_with_frontmatter
      return asset.attachment&.download || data if kind != 'text'

      result = ''
      # Use attributes['properties'] here, to always use content-local properties
      result += (YAML.dump(attributes['properties']) + "---\n") if attributes['properties'].present?

      result + data.to_s
    end

    # Data with frontmatter setter
    def data_with_frontmatter=(text)
      if kind == 'text'
        data_with_metadata = Scribo::Preamble.parse(text)
        self.properties = data_with_metadata.metadata
        self.data = data_with_metadata.content
      else
        self.data = text
      end
    end

    # Used for merging with defaults
    def properties
      attributes['properties']
      # defaults.merge(attributes['properties'] || {})
    end

    def properties=(text)
      props = text.is_a?(String) ? Scribo::Utility.yaml_safe_parse(text.gsub("\t", '  ')) : text
      write_attribute :properties, props
    end

    def type
      collection_name
    end

    def permalink
      properties&.[]('permalink')
    end

    def url
      result = permalink || Scribo::Utility.switch_extension(full_path)
      result += '/' unless result.end_with?('/')
      result
    end

    def date
      # return nil unless post?

      prop_date = begin
        Time.zone.parse(properties['date'])
      rescue StandardError
        nil
      end

      prop_date || post_date
    end

    def post_date
      Time.zone.strptime(path[0, 10], '%Y-%m-%d')
    rescue StandardError
      nil
    end

    def data
      return attributes['data'] if kind == 'asset'

      attributes['data']&.force_encoding('utf-8')
    end

    def excerpt
      # FIXME: This is a terrible implementation
      excerpt_part = data.gsub("\r\n", "\n\n").split("\n\n").reject(&:empty?).reject { |p| p.start_with?('#') }.first
      Scribo::ContentRenderService.new(self, {}, data: excerpt_part, layout: false).call
    end

    def render(context = {}, options = {})
      Scribo::ContentRenderService.new(self, context, options).call
    end

    def categories
      if properties&.[]('categories').is_a? Array
        properties&.[]('categories')
      else
        (properties&.[]('categories') || '').split
      end
    end

    def tags
      if properties&.[]('tags').is_a? Array
        properties&.[]('tags')
      else
        (properties&.[]('tags') || '').split
      end
    end

    def content_type
      properties&.[]('content_type') || mime_type&.content_type || 'application/octet-stream'
    end

    def media_type
      mime_type&.media_type
    end

    def extension
      mime_type&.extensions&.first
    end

    def mime_type
      MIME::Types.type_for(path).first
    end

    def dir
      File.dirname(full_path)
    end

    def extname
      File.extname(full_path).tr('.', '')
    end

    def translation_scope
      scope = File.dirname(full_path).split('/')
      scope << File.basename(full_path, File.extname(full_path))
      scope.join('.')
    end

    def cache_key
      "#{super}-#{updated_at}-#{I18n.locale}"
    end

    def collection_name
      return nil unless part_of_collection?

      ancestors.first.path[1..-1]
    end

    def part_of_collection?
      return false unless ancestors.first&.path&.start_with?('_')

      site.collections.include?(ancestors.first.path[1..-1])
    end

    def redirect?
      extname == 'link'
    end

    def layout?
      full_path.start_with?('/_layouts/')
    end

    def post?
      ancestors.map(&:path).join('/').start_with?('_posts')
    end

    def page?
      Scribo::Utility.output_content_type(self) == 'text/html'
    end

    def config?
      path == '_config.yml' && parent.nil?
    end

    def asset?
      kind == 'asset'
    end

    def folder?
      kind == 'folder'
    end

    def self.paginated?(path)
      path.match(%r[/(\d+)/$])
    end

    def self.search_paths_for(path)
      search_paths = []

      search_path = path
      search_path = "/#{search_path}" unless search_path.start_with?('/')
      search_path.gsub!(%r[/\d+/$], '/') if paginated?(search_path)
      search_path = "#{search_path}index.html" if search_path.ends_with?('/')

      search_paths.concat(alternative_paths_for(search_path))

      secondary_search_path = path.sub(%r[/$], '')
      secondary_search_path = "/#{secondary_search_path}" unless secondary_search_path.start_with?('/')
      search_paths.concat(alternative_paths_for(secondary_search_path)) if secondary_search_path != '' && secondary_search_path != search_path

      permalink_paths = [path]

      normalized_path = path
      normalized_path = "/#{normalized_path}" unless normalized_path.start_with?('/')
      normalized_path = "#{normalized_path}/" unless normalized_path.ends_with?('/')
      permalink_paths << normalized_path
      search_paths.concat(permalink_paths) # deal with permalinks

      search_paths.uniq
    end

    def self.alternative_paths_for(search_path)
      search_paths = []
      search_path = Scribo::Utility.switch_extension(search_path, 'html') unless File.extname(search_path).present?
      search_paths.concat(Scribo::Utility.variations_for_path(search_path))
      search_paths << Scribo::Utility.switch_extension(search_path, 'link')
    end

    def store_full_path(force = false)
      if force || saved_changes.include?(:path) || saved_changes.include?(:parent_id)
        if post?
          result = categories.join('/') + '/'
          result += date.strftime('%Y/%m/%d/') if date
          result += path[11..-1]
        elsif part_of_collection? && site.output_collection?(collection_name)
          result = "#{collection_name}/#{path}"
        else
          result = (ancestors.map(&:path) << path).join('/')
        end
        result = '/' + result unless result.start_with?('/')

        update_column(:full_path, result)

        children.reload.each do |child|
          child.store_full_path(true)
        end
      end
    end


    def tree_path
      result = (ancestors.map(&:path) << path).join('/')
      result = '/' + result unless result.start_with?('/')
      result
    end

    private

    def upload_asset
      return unless asset?
      # return unless data.present? -> Breaks with utf8?
      return unless data
      return unless data.size.positive?

      si = StringIO.new
      si.write(data)
      si.rewind

      asset.attach(io: si, filename: path, content_type: content_type)

      self.data = nil
    end

    def store_properties
      return unless attributes['data'].present?

      new_properties = Scribo::Utility.yaml_safe_parse(attributes['data'])
      old_properties = site.properties
      site.update(properties: new_properties)

      # Only reshuffle if they need to
      if old_properties['collections'] != new_properties['collections'] ||
         old_properties['permalink'] != new_properties['permalink']
        site.reshuffle!
      end
    end

    def post_path
      return unless post?

      errors.add(:path, 'path must be of format YYYY-MM-DD-title') unless path.match(/[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}-.*/)
    end

    def layout_cant_be_current_content
      return unless layout

      errors.add(:base, "layout can't be layout of itself") if layout.full_path == full_path
    end

    class << self
      def redirect_options(redirect_data)
        options = redirect_data.split
        if options.length == 2
          options[0] = options[0].to_i
        else
          options.unshift 302
        end
        options
      end
    end
  end
end