require 'etc'

include Process
module Jekyll
  class Site
    attr_reader :default_lang, :languages, :exclude_from_localization, :lang_vars, :lang_from_path, :fallback_canonical_to_default_lang
    attr_accessor :file_langs, :active_lang

    def prepare
      @file_langs = {}
      fetch_languages
      @parallel_localization = config.fetch('parallel_localization', true)
      @lang_from_path = config.fetch('lang_from_path', false)
      @fallback_canonical_to_default_lang = config.fetch('fallback_canonical_to_default_lang', false)
      @exclude_from_localization = config.fetch('exclude_from_localization', []).map do |e|
        if File.directory?(e) && e[-1] != '/'
          "#{e}/"
        else
          e
        end
      end
    end

    def fetch_languages
      @default_lang = config.fetch('default_lang', 'en')
      @languages = config.fetch('languages', ['en']).uniq
      @keep_files += (@languages - [@default_lang])
      @active_lang = @default_lang
      @lang_vars = config.fetch('lang_vars', [])
    end

    alias process_orig process
    def process
      prepare
      all_langs = ([@default_lang] + @languages).uniq
      if @parallel_localization
        nproc = Etc.nprocessors
        pids = {}
        begin
          all_langs.each do |lang|
            pids[lang] = fork do
              process_language lang
            end
            while pids.length >= (lang == all_langs[-1] ? 1 : nproc)
              sleep 0.1
              pids.map do |pid_lang, pid|
                next unless waitpid pid, Process::WNOHANG

                pids.delete pid_lang
                raise "Polyglot subprocess #{pid} (#{pid_lang}) failed (#{$?.exitstatus})" unless $?.success?
              end
            end
          end
        rescue Interrupt
          all_langs.each do |lang|
            next unless pids.key? lang

            puts "Killing #{pids[lang]} : #{lang}"
            kill('INT', pids[lang])
          end
        end
      else
        all_langs.each do |lang|
          process_language lang
        end
      end
      Jekyll::Hooks.trigger :polyglot, :post_write, self
    end

    alias site_payload_orig site_payload
    def site_payload
      payload = site_payload_orig
      payload['site']['default_lang'] = default_lang
      payload['site']['languages'] = languages
      payload['site']['active_lang'] = active_lang
      lang_vars.each do |v|
        payload['site'][v] = active_lang
      end
      payload
    end

    def process_language(lang)
      @active_lang = lang
      config['active_lang'] = @active_lang
      lang_vars.each do |v|
        config[v] = @active_lang
      end
      if @active_lang == @default_lang
      then process_default_language
      else
        process_active_language
      end
    end

    def process_default_language
      old_include = @include
      process_orig
      @include = old_include
    end

    def process_active_language
      old_dest = @dest
      old_exclude = @exclude
      @file_langs = {}
      @dest = "#{@dest}/#{@active_lang}"
      @exclude += @exclude_from_localization
      process_orig
      @dest = old_dest
      @exclude = old_exclude
    end

    def split_on_multiple_delimiters(string)
      delimiters = ['.', '/']
      regex = Regexp.union(delimiters)
      string.split(regex)
    end

    # Convert glob pattern to regex pattern
    # * matches any characters except /
    # ? matches any single character except /
    def glob_to_regex(pattern)
      # Escape special regex characters first
      escaped = Regexp.escape(pattern)
      # Convert glob patterns to regex patterns
      escaped.gsub("\\*", '.*').gsub("\\?", '.')
    end

    def derive_lang_from_path(doc)
      unless @lang_from_path
        return nil
      end

      segments = split_on_multiple_delimiters(doc.path)
      segments.each do |segment|
        match = @languages.find { |lang| lang.downcase == segment.downcase }
        return match if match
      end

      nil
    end

    # assigns natural permalinks to documents and prioritizes documents with
    # active_lang languages over others.  If lang is not set in front matter,
    # then this tries to derive from the path, if the lang_from_path is set.
    # otherwise it will assign the document to the default_lang
    def coordinate_documents(docs)
      regex = document_url_regex
      approved = {}
      # Build set of valid languages (default + configured)
      valid_languages = ([@default_lang] + @languages).uniq

      docs.each do |doc|
        # Get the explicitly declared language (frontmatter or path-derived)
        explicit_lang = doc.data['lang'] || derive_lang_from_path(doc)
        lang = explicit_lang || @default_lang

        # FILTER: Skip documents whose explicit lang is not in configured languages.
        # Check the explicit value (not the fallback) so that documents with an
        # unconfigured lang like 'de' are excluded even if normalization would
        # map them to default_lang. Compare case-insensitively so case-mismatched
        # frontmatter (e.g. 'pt-br' vs configured 'pt-BR') is normalized below
        # rather than rejected here.
        if explicit_lang && valid_languages.none? { |l| l.downcase == explicit_lang.downcase }
          Jekyll.logger.warn "Polyglot:", "Skipping #{doc.relative_path} - lang '#{explicit_lang}' not in configured languages #{valid_languages.inspect}"
          next
        end

        # If the doc lang matches a config language case-insensitively, use the config case
        config_lang = @languages.find { |l| l.downcase == lang.downcase }
        lang = config_lang if config_lang
        doc.data['lang'] = lang if doc.data['lang'] && config_lang

        lang_exclusive = doc.data['lang-exclusive'] || []

        url = doc.url.gsub(regex, '/')
        page_id = doc.data['page_id'] || url
        doc.data['permalink'] = url if doc.data['permalink'].to_s.empty? && !doc.data['lang'].to_s.empty?
        # Set rendered_lang to indicate what language this page is actually rendered in
        # This allows templates to detect fallback pages (rendered_lang != active_lang)
        doc.data['rendered_lang'] = lang

        # skip entirely if nothing to check
        next if @file_langs.nil?
        # skip this document if it has already been processed
        next if @file_langs[page_id] == @active_lang
        # skip this document if it has a fallback and it isn't assigned to the active language
        next if @file_langs[page_id] == @default_lang && lang != @active_lang
        # skip this document if it has lang-exclusive defined and the active_lang is not included
        next if !lang_exclusive.empty? && !lang_exclusive.include?(@active_lang)

        approved[page_id] = doc
        @file_langs[page_id] = lang
      end
      approved.each_value do |doc|
        assignPageRedirects(doc, docs)
        assignPageLanguagePermalinks(doc, docs)
      end
      approved.values
    end

    def assignPageRedirects(doc, docs)
      # Preserve and normalize user-defined redirect_from
      user_redirects = doc.data['redirect_from'] || []
      user_redirects = [user_redirects] unless user_redirects.is_a?(Array)

      # Determine document language
      doc_lang = doc.data['lang'] || derive_lang_from_path(doc) || @default_lang

      # Scope user-defined redirects to document's language if non-default
      if doc_lang != @default_lang && !user_redirects.empty?
        user_redirects = user_redirects.map do |redirect_path|
          # Normalize path to start with /
          redirect_path = "/#{redirect_path}" unless redirect_path.start_with?('/')
          # Only prefix if not already prefixed with this language
          if redirect_path.start_with?("/#{doc_lang}/")
            redirect_path
          else
            "/#{doc_lang}#{redirect_path}"
          end
        end
      end

      # Compute page_id based redirects (cross-language)
      computed_redirects = []
      pageId = doc.data['page_id']
      if !pageId.nil? && !pageId.empty?
        docs_with_same_id = docs.select { |dd| dd.data['page_id'] == pageId }
        docs_with_same_id.each do |dd|
          if dd.data['permalink'] != doc.data['permalink']
            computed_redirects << dd.data['permalink']
          end
        end
      end

      # Merge user-defined and computed redirects, removing duplicates
      all_redirects = (user_redirects + computed_redirects).uniq
      doc.data['redirect_from'] = all_redirects unless all_redirects.empty?
    end

    def assignPageLanguagePermalinks(doc, docs)
      page_id = doc.data['page_id']
      normalized_permalink = normalized_permalink_for_doc(doc)
      translations = find_translations(page_id, normalized_permalink, docs)

      doc.data['permalink_lang'] = translations
      configured = ([@default_lang] + @languages).uniq
      doc.data['available_languages'] = translations.keys
      # missing_languages signals "a visitor in this lang would see different
      # content than another lang's visitor". A single-source page falls back
      # identically everywhere, so nothing is missing in that case.
      doc.data['missing_languages'] =
        translations.size > 1 ? (configured - translations.keys) : []
    end

    # Returns a hash of { lang => permalink } for all docs that are translations
    # of the given page. Matches by page_id when present, otherwise by normalized
    # permalink. Filters out languages not in the configured languages list.
    # candidate_docs defaults to site.collections + site.pages so the helper can
    # be called from Liquid render contexts where the caller doesn't already
    # hold a docs array.
    def find_translations(page_id, normalized_permalink, candidate_docs = nil)
      candidate_docs ||= collections.values.flat_map(&:docs) + pages
      valid_languages = ([@default_lang] + @languages).uniq

      matching =
        if !page_id.to_s.empty?
          candidate_docs.select { |d| d.data['page_id'] == page_id }
        elsif !normalized_permalink.to_s.empty?
          candidate_docs.select { |d| normalized_permalink_for_doc(d) == normalized_permalink }
        else
          []
        end

      matching.each_with_object({}) do |d, h|
        explicit_lang = d.data['lang'] || derive_lang_from_path(d)
        doclang = explicit_lang || @default_lang
        next if explicit_lang && !valid_languages.include?(explicit_lang)

        h[doclang] = d.data['permalink']
      end
    end

    # Returns the doc's permalink with its own language prefix stripped, so it
    # can be matched against sibling docs that share the same un-prefixed
    # permalink. Returns nil when no usable permalink is present.
    def normalized_permalink_for_doc(doc)
      permalink = doc.data['permalink'] || (doc.respond_to?(:url) ? doc.url : nil)
      return nil if permalink.to_s.empty?

      permalink = "/#{permalink}" unless permalink.start_with?('/')
      lang = doc.data['lang']
      return permalink if lang.to_s.empty?

      stripped = permalink.delete_prefix("/#{lang}/")
      stripped.start_with?('/') ? stripped : "/#{stripped}"
    end

    # performs any necessary operations on the documents before rendering them
    def process_documents(docs)
      # return if @active_lang == @default_lang

      url = config.fetch('url', false)
      rel_regex = relative_url_regex(false)
      abs_regex = absolute_url_regex(url, false)
      non_rel_regex = relative_url_regex(true)
      non_abs_regex = absolute_url_regex(url, true)
      docs.each do |doc|
        unless @active_lang == @default_lang then relativize_urls(doc, rel_regex) end
        correct_nonrelativized_urls(doc, non_rel_regex)
        if url
          unless @active_lang == @default_lang then relativize_absolute_urls(doc, abs_regex, url) end
          correct_nonrelativized_absolute_urls(doc, non_abs_regex, url)
        end
      end
    end

    # a regex that matches urls or permalinks with i18n prefixes or suffixes
    # matches /en/foo , .en/foo , foo.en/ and other simmilar default urls
    # made by jekyll when parsing documents without explicitly set permalinks
    def document_url_regex
      regex = ''
      (@languages || []).each do |lang|
        regex += "([/.]#{lang}[/.])|"
      end
      regex.chomp! '|'
      /#{regex}/
    end

    # a regex that matches relative urls in a html document
    # matches href="baseurl/foo/bar-baz" href="/foo/bar-baz" and others like it
    # avoids matching excluded files.  prepare makes sure
    # that all @exclude dirs have a trailing slash.
    def relative_url_regex(disabled = false)
      regex = ''
      unless disabled
        @exclude.each do |x|
          escaped_x = glob_to_regex(x)
          regex += "(?!#{escaped_x})"
        end
        @languages.each do |x|
          escaped_x = Regexp.escape(x)
          regex += "(?!#{escaped_x}/)"
        end
      end
      start = disabled ? 'ferh' : 'href'
      %r{#{start}="?#{@baseurl}/((?:#{regex}[^,'"\s/?.]+\.?)*(?:/[^\]\[)("'\s]*)?)"}
    end

    # a regex that matches absolute urls in a html document
    # matches href="http://baseurl/foo/bar-baz" and others like it
    # avoids matching excluded files.  prepare makes sure
    # that all @exclude dirs have a trailing slash.
    def absolute_url_regex(url, disabled = false)
      regex = ''
      unless disabled
        @exclude.each do |x|
          escaped_x = glob_to_regex(x)
          regex += "(?!#{escaped_x})"
        end
        @languages.each do |x|
          escaped_x = Regexp.escape(x)
          regex += "(?!#{escaped_x}/)"
        end
      end
      start = disabled ? 'ferh' : 'href'
      # Build negative lookbehind to exclude hreflang URLs from relativization
      # hreflang tags for default language and x-default should not be relativized
      neglookbehind = disabled ? "" : "(?<!hreflang=\"#{@default_lang}\" |hreflang=\"x-default\" )"
      %r{#{neglookbehind}#{start}="?#{url}#{@baseurl}/((?:#{regex}[^,'"\s/?.]+\.?)*(?:/[^\]\[)("'\s]*)?)"}
    end

    def relativize_urls(doc, regex)
      return if doc.output.nil?

      modified_output = doc.output.dup
      modified_output.gsub!(regex, "href=\"#{@baseurl}/#{@active_lang}/\\1\"")
      doc.output = modified_output
    end

    def relativize_absolute_urls(doc, regex, url)
      return if doc.output.nil?

      modified_output = doc.output.dup
      modified_output.gsub!(regex, "href=\"#{url}#{@baseurl}/#{@active_lang}/\\1\"")
      doc.output = modified_output
    end

    def correct_nonrelativized_absolute_urls(doc, regex, url)
      return if doc.output.nil?

      modified_output = doc.output.dup
      modified_output.gsub!(regex, "href=\"#{url}#{@baseurl}/\\1\"")
      doc.output = modified_output
    end

    def correct_nonrelativized_urls(doc, regex)
      return if doc.output.nil?

      modified_output = doc.output.dup
      modified_output.gsub!(regex, "href=\"#{@baseurl}/\\1\"")
      doc.output = modified_output
    end
  end
end
