# frozen_string_literal: true

module JekyllImport
  module Importers
    class MT < Importer
      SUPPORTED_ENGINES = %w(mysql postgres sqlite).freeze

      STATUS_DRAFT = 1
      STATUS_PUBLISHED = 2
      MORE_CONTENT_SEPARATOR = "<!--more-->"

      def self.default_options
        {
          "blog_id"       => nil,
          "categories"    => true,
          "dest_encoding" => "utf-8",
          "src_encoding"  => "utf-8",
          "comments"      => false,
        }
      end

      def self.require_deps
        JekyllImport.require_with_fallback(%w(
          rubygems
          sequel
          sqlite3
          mysql2
          pg
          fileutils
          safe_yaml
        ))
      end

      def self.specify_options(c)
        c.option "dbname",        "--dbname DB",              "Database name."
        c.option "user",          "--user USER",              "Database user name."
        c.option "engine",        "--engine ENGINE",          "Database engine ('mysql' or 'postgres'). (default: 'mysql')"
        c.option "password",      "--password PW",            "Database user's password. (default: '')"
        c.option "host",          "--host HOST",              "Database host name. (default: 'localhost')"
        c.option "port",          "--port PORT",              "Custom database port connect to. (default: null)"
        c.option "blog_id",       "--blog_id ID",             "Specify a single Movable Type blog ID to import. (default: null (all blogs))"
        c.option "categories",    "--categories",             "When true, save post's categories in its YAML front matter. (default: true)"
        c.option "src_encoding",  "--src_encoding ENCODING",  "Encoding of strings from database. (default: UTF-8)"
        c.option "dest_encoding", "--dest_encoding ENCODING", "Encoding of output strings. (default: UTF-8)"
        c.option "comments",      "--comments",               "When true, output comments in `_comments` directory. (default: false)"
      end

      # By default this migrator will include posts for all your MovableType blogs.
      # Specify a single blog by providing blog_id.

      # Main migrator function. Call this to perform the migration.
      #
      # dbname::  The name of the database
      # user::    The database user name
      # pass::    The database user's password
      # host::    The address of the MySQL database host. Default: 'localhost'
      # options:: A hash of configuration options
      #
      # Supported options are:
      #
      # blog_id::         Specify a single MovableType blog to export by providing blog_id.
      #                   Default: nil, importer will include posts for all blogs.
      # categories::      If true, save the post's categories in its
      #                   YAML front matter. Default: true
      # src_encoding::    Encoding of strings from the database. Default: UTF-8
      #                   If your output contains mangled characters, set src_encoding to
      #                   something appropriate for your database charset.
      # dest_encoding::   Encoding of output strings. Default: UTF-8
      # comments::        If true, output comments in _comments directory, like the one
      #                   mentioned at https://github.com/mpalmer/jekyll-static-comments/
      def self.process(options)
        options = default_options.merge(options)

        comments = options.fetch("comments")
        posts_name_by_id = {} if comments

        db = database_from_opts(options)

        post_categories = db[:mt_placement].join(:mt_category, :category_id => :placement_category_id)

        FileUtils.mkdir_p "_posts"

        posts = db[:mt_entry]
        posts = posts.filter(:entry_blog_id => options["blog_id"]) if options["blog_id"]
        posts.each do |post|
          categories = post_categories.filter(
            :placement_entry_id => post[:entry_id]
          ).map { |ea| encode(ea[:category_basename], options) }

          file_name = post_file_name(post, options)

          data = post_metadata(post, options)
          data["categories"] = categories if !categories.empty? && options["categories"]
          yaml_front_matter = data.delete_if { |_, v| v.nil? || v == "" }.to_yaml

          # save post path for comment processing
          posts_name_by_id[data["post_id"]] = file_name if comments

          content = post_content(post, options)

          File.open("_posts/#{file_name}", "w") do |f|
            f.puts yaml_front_matter
            f.puts "---"
            f.puts encode(content, options)
          end
        end

        # process comment output, if enabled
        if comments
          FileUtils.mkdir_p "_comments"

          comments = db[:mt_comment]
          comments.each do |comment|
            next unless posts_name_by_id.key?(comment[:comment_entry_id]) # if the entry exists

            dir_name, base_name = comment_file_dir_and_base_name(posts_name_by_id, comment, options)
            FileUtils.mkdir_p "_comments/#{dir_name}"

            data = comment_metadata(comment, options)
            content = comment_content(comment, options)
            yaml_front_matter = data.delete_if { |_, v| v.nil? || v == "" }.to_yaml

            File.open("_comments/#{dir_name}/#{base_name}", "w") do |f|
              f.puts yaml_front_matter
              f.puts "---"
              f.puts encode(content, options)
            end
          end
        end
      end

      # Extracts metadata for YAML front matter from post
      def self.post_metadata(post, options = default_options)
        metadata = {
          "layout"   => "post",
          "title"    => encode(post[:entry_title], options),
          "date"     => post_date(post).strftime("%Y-%m-%d %H:%M:%S %z"),
          "excerpt"  => encode(post[:entry_excerpt].to_s, options),
          "mt_id"    => post[:entry_id],
          "blog_id"  => post[:entry_blog_id],
          "post_id"  => post[:entry_id], # for link with comments
          "basename" => post[:entry_basename],
        }
        metadata["published"] = false if post[:entry_status] != STATUS_PUBLISHED
        metadata
      end

      # Different versions of MT used different column names
      def self.post_date(post)
        post[:entry_authored_on] || post[:entry_created_on]
      end

      # Extracts text body from post
      def self.extra_entry_text_empty?(post)
        post[:entry_text_more].nil? || post[:entry_text_more].strip.empty?
      end

      def self.post_content(post, _options = default_options)
        if extra_entry_text_empty?(post)
          post[:entry_text]
        else
          post[:entry_text] + "\n\n#{MORE_CONTENT_SEPARATOR}\n\n" + post[:entry_text_more]
        end
      end

      def self.post_file_name(post, _options = default_options)
        date = post_date(post)
        slug = post[:entry_basename]
        file_ext = suffix(post[:entry_convert_breaks])

        "#{date.strftime("%Y-%m-%d")}-#{slug}.#{file_ext}"
      end

      # Extracts metadata for YAML front matter from comment
      def self.comment_metadata(comment, options = default_options)
        metadata = {
          "layout"       => "comment",
          "comment_id"   => comment[:comment_id],
          "post_id"      => comment[:comment_entry_id],
          "author"       => encode(comment[:comment_author], options),
          "email"        => comment[:comment_email],
          "commenter_id" => comment[:comment_commenter_id],
          "date"         => comment_date(comment).strftime("%Y-%m-%d %H:%M:%S %z"),
          "visible"      => comment[:comment_visible] == 1,
          "ip"           => comment[:comment_ip],
          "url"          => comment[:comment_url],
        }
        metadata
      end

      # Different versions of MT used different column names
      def self.comment_date(comment)
        comment[:comment_modified_on] || comment[:comment_created_on]
      end

      def self.comment_content(comment, _options = default_options)
        comment[:comment_text]
      end

      def self.comment_file_dir_and_base_name(posts_name_by_id, comment, _options = default_options)
        post_basename = posts_name_by_id[comment[:comment_entry_id]].sub(%r!\.\w+$!, "")
        comment_id = comment[:comment_id]

        [post_basename, "#{comment_id}.markdown"]
      end

      def self.encode(str, options = default_options)
        if str.respond_to?(:encoding)
          str.encode(options["dest_encoding"], options["src_encoding"])
        else
          str
        end
      end

      # Ideally, this script would determine the post format (markdown,
      # html, etc) and create files with proper extensions. At this point
      # it just assumes that markdown will be acceptable.
      def self.suffix(entry_type)
        if entry_type.nil? || entry_type.include?("markdown") || entry_type.include?("__default__")
          # The markdown plugin I have saves this as
          # "markdown_with_smarty_pants", so I just look for "markdown".
          "markdown"
        elsif entry_type.include?("textile")
          # This is saved as "textile_2" on my installation of MT 5.1.
          "textile"
        elsif entry_type == "0" || entry_type.include?("richtext")
          # Richtext looks to me like it's saved as HTML, so I include it here.
          "html"
        else
          # Other values might need custom work.
          entry_type
        end
      end

      def self.database_from_opts(options)
        engine        = options.fetch("engine", "mysql")
        dbname        = options.fetch("dbname")
        sequel_engine = engine == "mysql" ? "mysql2" : engine

        case engine
        when "sqlite"
          Sequel.sqlite(dbname)
        when "mysql", "postgres"
          db_connect_opts = {
            :host     => options.fetch("host", "127.0.0.1"),
            :user     => options.fetch("user"),
            :password => options.fetch("password", ""),
          }
          db_connect_opts = options["port"] if options["port"]
          Sequel.public_send(
            sequel_engine,
            dbname,
            db_connect_opts
          )
        else
          abort("Unsupported engine: '#{engine}'. Must be one of #{SUPPORTED_ENGINES.join(", ")}")
        end
      end
    end
  end
end
