# typed: strict
module Domain::PostsHelper
  extend T::Sig
  extend T::Helpers
  include HelpersInterface
  include LogEntriesHelper
  include Domain::UsersHelper
  include PathsHelper
  include Domain::DomainsHelper
  include Domain::DomainModelHelper
  include Pundit::Authorization
  require "base64"
  abstract!

  class DomainData < T::Struct
    include T::Struct::ActsAsComparable

    const :domain_icon_path, String
    const :domain_icon_title, String
  end

  DEFAULT_DOMAIN_DATA =
    DomainData.new(
      domain_icon_path: "generic-domain.svg",
      domain_icon_title: "Unknown",
    )

  DOMAIN_DATA =
    T.let(
      {
        Domain::DomainType::Fa =>
          DomainData.new(
            domain_icon_path: "domain-icons/fa.png",
            domain_icon_title: "Furaffinity",
          ),
        Domain::DomainType::E621 =>
          DomainData.new(
            domain_icon_path: "domain-icons/e621.png",
            domain_icon_title: "E621",
          ),
        Domain::DomainType::Inkbunny =>
          DomainData.new(
            domain_icon_path: "domain-icons/inkbunny.png",
            domain_icon_title: "Inkbunny",
          ),
        Domain::DomainType::Sofurry =>
          DomainData.new(
            domain_icon_path: "domain-icons/sofurry.png",
            domain_icon_title: "SoFurry",
          ),
      },
      T::Hash[Domain::DomainType, DomainData],
    )

  sig { params(model: HasDomainType).returns(String) }
  def domain_model_icon_path(model)
    path =
      if (domain_data = DOMAIN_DATA[model.domain_type])
        domain_data.domain_icon_path
      else
        DEFAULT_DOMAIN_DATA.domain_icon_path
      end
    asset_path(path)
  end

  sig { params(post: Domain::Post).returns(T.nilable(Domain::PostFile)) }
  def gallery_file_for_post(post)
    file = post.primary_file_for_view
    return nil unless file.present?
    return nil unless file.state_ok?
    return nil unless file.log_entry_id.present?
    content_type = file.log_entry&.content_type
    return nil unless content_type.present?
    return nil unless is_thumbable_content_type?(content_type)
    file
  end

  sig { params(post: Domain::Post).returns(T.nilable(String)) }
  def gallery_file_info_for_post(post)
    file = post.primary_file_for_view
    return nil unless file.present?
    return nil unless file.state_ok?
    return nil unless file.log_entry_id.present?
    content_type = file.log_entry&.content_type || ""
    pretty_content_type(content_type)
  end

  sig { params(post: Domain::Post).returns(T.nilable(String)) }
  def thumbnail_for_post_path(post)
    return nil unless policy(post).view_file?
    file = gallery_file_for_post(post)
    return nil unless file.present?
    return nil unless file.state_ok?
    return nil unless file.log_entry_id.present?
    if (log_entry = file.log_entry) &&
         (response_sha256 = log_entry.response_sha256)
      blob_path(HexUtil.bin2hex(response_sha256), format: "jpg", thumb: "small")
    end
  end

  # Create a data URI thumbnail from an image file
  sig do
    params(file_path: String, content_type: String, max_size: Integer).returns(
      T.nilable(String),
    )
  end
  def create_image_thumbnail_data_uri(file_path, content_type, max_size = 180)
    # Load the Vips library properly instead of using require directly
    begin
      # Load the image
      image = ::Vips::Image.new_from_file(file_path)

      # Calculate the scaling factor to keep within max_size
      scale = [max_size.to_f / image.width, max_size.to_f / image.height].min

      # Only scale down, not up
      scale = 1.0 if scale > 1.0

      # Resize the image (use nearest neighbor for speed as this is just a thumbnail)
      thumbnail = image.resize(scale)

      # Get the image data in the original format
      # For JPEG use quality 85 for a good balance of quality vs size
      output_format = content_type.split("/").last

      case output_format
      when "jpeg", "jpg"
        image_data = thumbnail.write_to_buffer(".jpg", Q: 85)
      when "png"
        image_data = thumbnail.write_to_buffer(".png", compression: 6)
      when "gif"
        image_data = thumbnail.write_to_buffer(".gif")
      else
        # Default to JPEG if format not recognized
        image_data = thumbnail.write_to_buffer(".jpg", Q: 85)
        content_type = "image/jpeg"
      end

      # Create the data URI
      "data:#{content_type};base64,#{Base64.strict_encode64(image_data)}"
    rescue => e
      Rails.logger.error("Error creating thumbnail: #{e.message}")
      nil
    end
  end

  sig { params(content_type: String).returns(String) }
  def pretty_content_type(content_type)
    case content_type
    when %r{text/plain}
      "Plain Text Document"
    when %r{application/pdf}
      "PDF Document"
    when %r{application/msword}
      "Microsoft Word Document"
    when %r{application/vnd\.openxmlformats-officedocument\.wordprocessingml\.document}
      "Microsoft Word Document (OpenXML)"
    when %r{application/vnd\.oasis\.opendocument\.text}
      "OpenDocument Text"
    when %r{application/rtf}
      "Rich Text Document"
    when %r{image/jpeg}
      "JPEG Image"
    when %r{image/png}
      "PNG Image"
    when %r{image/gif}
      "GIF Image"
    when %r{video/webm}
      "Webm Video"
    when %r{audio/mpeg}
      "MP3 Audio"
    when %r{audio/mp3}
      "MP3 Audio"
    when %r{audio/wav}
      "WAV Audio"
    else
      content_type.split(";").first&.split("/")&.last&.titleize || "Unknown"
    end
  end

  sig { params(post: Domain::Post).returns(T.nilable(String)) }
  def gallery_file_size_for_post(post)
    file = post.primary_file_for_view
    return nil unless file.present?
    return nil unless file.state_ok?
    return nil unless file.log_entry_id.present?
    file.log_entry&.response_size&.then { |size| number_to_human_size(size) }
  end

  sig { params(url: String).returns(T.nilable(String)) }
  def icon_asset_for_url(url)
    domain = extract_domain(url)
    return nil unless domain
    icon_path_for_domain(domain)
  end

  sig { params(category: Symbol).returns(String) }
  def tailwind_tag_category_class(category)
    case category
    when :general
      "bg-blue-300" # Light blue
    when :artist
      "bg-indigo-300" # Light indigo
    when :copyright
      "bg-purple-300" # Light purple
    when :character
      "bg-green-300" # Light green
    when :species
      "bg-teal-300" # Light teal
    when :invalid
      "bg-slate-300" # Medium gray
    when :meta
      "bg-amber-300" # Light amber
    when :lore
      "bg-cyan-300" # Light cyan
    else
      "bg-white" # White (default)
    end
  end

  sig { returns(T::Array[Symbol]) }
  def tag_category_order
    TAG_CATEGORY_ORDER
  end

  sig { params(category: Symbol).returns(T.nilable(String)) }
  def font_awesome_category_icon(category)
    case category
    when :artist
      "fa-brush"
    when :species
      "fa-paw"
    when :character
      "fa-user"
    when :copyright
      "fa-copyright"
    when :general
      "fa-tag"
    when :lore
      "fa-book"
    when :meta
      "fa-info"
    when :invalid
      "fa-ban"
    end
  end

  class LinkForSource < T::ImmutableStruct
    include T::Struct::ActsAsComparable

    const :model, ReduxApplicationRecord
    const :title, String
    const :model_path, String
    const :icon_path, T.nilable(String)
  end

  class SourceResult < T::ImmutableStruct
    include T::Struct::ActsAsComparable

    const :model, ReduxApplicationRecord
    const :title, String
  end

  class SourceMatcher < T::ImmutableStruct
    extend T::Generic
    include T::Struct::ActsAsComparable

    const :hosts, T::Array[String]
    const :patterns, T::Array[Regexp]
    const :find_proc,
          T
            .proc
            .params(helper: Domain::PostsHelper, match: MatchData, url: String)
            .returns(T.nilable(SourceResult))
  end

  FA_HOSTS = %w[*.furaffinity.net furaffinity.net]
  FA_CDN_HOSTS = %w[d.furaffinity.net *.facdn.net facdn.net]
  IB_HOSTS = %w[*.inkbunny.net inkbunny.net]
  IB_CDN_HOSTS = %w[*.ib.metapix.net ib.metapix.net]
  E621_HOSTS = %w[www.e621.net e621.net]

  URL_SUFFIX_QUERY = T.let(<<-SQL.strip.chomp.freeze, String)
    lower('url_str') = lower(?)
  SQL

  MATCHERS =
    T.let(
      [
        # Furaffinity posts
        SourceMatcher.new(
          hosts: FA_HOSTS,
          patterns: [
            %r{/view/(\d+)/?},
            %r{/full/(\d+)/?},
            %r{/controls/submissions/changeinfo/(\d+)/?},
          ],
          find_proc: ->(helper, match, _) do
            if post = Domain::Post::FaPost.find_by(fa_id: match[1])
              SourceResult.new(
                model: post,
                title: helper.title_for_post_model(post),
              )
            end
          end,
        ),
        # Furaffinity posts via direct file URL
        SourceMatcher.new(
          hosts: FA_CDN_HOSTS,
          patterns: [/.+/],
          find_proc: ->(helper, _, url) do
            url = Addressable::URI.parse(url)

            post_file =
              Domain::PostFile.where(
                "lower('url_str') IN (?, ?, ?, ?, ?, ?)",
                "d.furaffinity.net#{url.host}/#{url.path}",
                "//d.furaffinity.net#{url.host}/#{url.path}",
                "https://d.furaffinity.net#{url.host}/#{url.path}",
                "d.facdn.net#{url.host}/#{url.path}",
                "//d.facdn.net#{url.host}/#{url.path}",
                "https://d.facdn.net#{url.host}/#{url.path}",
              ).first

            if post_file && (post = post_file.post)
              title =
                T.bind(self, Domain::PostsHelper).title_for_post_model(post)
              SourceResult.new(model: post, title:)
            end
          end,
        ),
        # Furaffinity users
        SourceMatcher.new(
          hosts: FA_HOSTS,
          patterns: [%r{/user/([^/]+)/?}],
          find_proc: ->(helper, match, _) do
            if user = Domain::User::FaUser.find_by(url_name: match[1])
              SourceResult.new(
                model: user,
                title: user.name_for_view || "unknown",
              )
            end
          end,
        ),
        # Inkbunny posts
        SourceMatcher.new(
          hosts: IB_HOSTS,
          patterns: [%r{/s/(\d+)/?}, %r{/submissionview\.php\?id=(\d+)/?}],
          find_proc: ->(helper, match, _) do
            if post = Domain::Post::InkbunnyPost.find_by(ib_id: match[1])
              title = helper.title_for_post_model(post)
              SourceResult.new(model: post, title:)
            end
          end,
        ),
        # Inkbunny posts via direct file URL
        SourceMatcher.new(
          hosts: IB_CDN_HOSTS,
          patterns: [//],
          find_proc: ->(helper, _, url) do
            url = Addressable::URI.parse(url)
            if post_file =
                 Domain::PostFile.where(
                   "#{URL_SUFFIX_QUERY}",
                   "ib.metapix.net#{url.path}",
                 ).first
              if post = post_file.post
                title = helper.title_for_post_model(post)
                SourceResult.new(model: post, title:)
              end
            end
          end,
        ),
        # Inkbunny users
        SourceMatcher.new(
          hosts: IB_HOSTS,
          patterns: [%r{/(\w+)/?$}],
          find_proc: ->(_, match, _) do
            if user =
                 Domain::User::InkbunnyUser.where(
                   "name = lower(?)",
                   match[1],
                 ).first
              SourceResult.new(
                model: user,
                title: user.name_for_view || "unknown",
              )
            end
          end,
        ),
        # E621 posts
        SourceMatcher.new(
          hosts: E621_HOSTS,
          patterns: [%r{/posts/(\d+)/?}],
          find_proc: ->(helper, match, _) do
            if post = Domain::Post::E621Post.find_by(e621_id: match[1])
              SourceResult.new(
                model: post,
                title: helper.title_for_post_model(post),
              )
            end
          end,
        ),
        # E621 users
        SourceMatcher.new(
          hosts: E621_HOSTS,
          patterns: [%r{/users/(\d+)/?}],
          find_proc: ->(helper, match, _) do
            if user = Domain::User::E621User.find_by(e621_id: match[1])
              SourceResult.new(
                model: user,
                title: user.name_for_view || "unknown",
              )
            end
          end,
        ),
      ],
      T::Array[SourceMatcher],
    )

  sig { params(source: String).returns(T.nilable(LinkForSource)) }
  def link_for_source(source)
    return nil if source.blank?

    # normalize the source to a lowercase string with a protocol
    source.downcase!
    source = "https://" + source unless source.include?("://")
    begin
      uri = URI.parse(source)
    rescue StandardError
      return nil
    end
    uri_host = uri.host
    return nil if uri_host.blank?

    for matcher in MATCHERS
      if matcher.hosts.any? { |host|
           File.fnmatch?(host, uri_host, File::FNM_PATHNAME)
         }
        for pattern in matcher.patterns
          if (match = pattern.match(uri.to_s))
            object = matcher.find_proc.call(self, match, uri.to_s)
            return nil unless object
            model = object.model

            if model.is_a?(Domain::Post)
              model_path =
                Rails.application.routes.url_helpers.domain_post_path(model)
            elsif model.is_a?(Domain::User)
              model_path =
                Rails.application.routes.url_helpers.domain_user_path(model)
              icon_path =
                domain_user_avatar_img_src_path(
                  model.avatar,
                  thumb: "64-avatar",
                )
            else
              model_path = "#"
            end

            return(
              LinkForSource.new(
                model:,
                title: object.title,
                model_path:,
                icon_path:,
              )
            )
          end
        end
      end
    end
    nil
  end

  sig { params(post: Domain::Post::FaPost).returns(T::Array[String]) }
  def keywords_for_fa_post(post)
    post.keywords.map(&:strip).reject(&:blank?).compact
  end

  sig { params(post: Domain::Post::InkbunnyPost).returns(T::Array[String]) }
  def keywords_for_ib_post(post)
    post.keywords.map { |keyword| keyword["keyword_name"] }.compact
  end

  sig do
    params(time: T.nilable(T.any(ActiveSupport::TimeWithZone, Time))).returns(
      String,
    )
  end
  def time_ago_in_words_no_prefix(time)
    return "never" if time.nil?
    time = time.in_time_zone if time.is_a?(Time)
    time_ago_in_words(time).delete_prefix("over ").delete_prefix("about ")
  end

  sig do
    params(faved_at_type: Domain::UserPostFav::FavedAtType).returns(String)
  end
  def faved_at_type_icon(faved_at_type)
    case faved_at_type
    when Domain::UserPostFav::FavedAtType::PostedAt
      "fa-clock" # Clock icon for fallback to posted_at
    when Domain::UserPostFav::FavedAtType::Explicit
      "fa-calendar-check" # Calendar check for explicitly set time
    when Domain::UserPostFav::FavedAtType::Inferred
      "fa-chart-line" # Chart line for inferred from regression model
    when Domain::UserPostFav::FavedAtType::InferredNow
      "fa-bolt" # Lightning bolt for computed on the fly
    end
  end

  sig do
    params(faved_at_type: Domain::UserPostFav::FavedAtType).returns(String)
  end
  def faved_at_type_tooltip(faved_at_type)
    case faved_at_type
    when Domain::UserPostFav::FavedAtType::PostedAt
      "Estimated from posted date"
    when Domain::UserPostFav::FavedAtType::Explicit
      "Exact time recorded"
    when Domain::UserPostFav::FavedAtType::Inferred
      "Estimated from regression model"
    when Domain::UserPostFav::FavedAtType::InferredNow
      "Estimated in real-time from regression model"
    end
  end

  private

  sig { params(url: String).returns(T.nilable(String)) }
  def extract_domain(url)
    URI.parse(url).host
  rescue URI::InvalidURIError
    nil
  end

  TAG_CATEGORY_ORDER =
    T.let(
      %i[artist copyright character species general meta lore invalid].freeze,
      T::Array[Symbol],
    )
end
