# frozen_string_literal: true

require "erb"
require "fileutils"
require "json"
require "generator/logger"
require "generator/config"
require "generator/formatter"
require "generator/file_writer"
require "generator/json_schema_extractor"
require "generator/type"
require "generator/type_resolver"
require "generator/rbs/types"

module Generator
  # Generates Ruby type classes for SP-API notification payloads from JSON Schema files
  class Notification
    include FileWriter
    include Formatter

    attr_reader :file_path, :schema

    class << self
      def generate(filter: nil)
        cleanup! unless filter
        notifications = filter ? filtered_notifications(filter) : all_notifications
        notifications.each(&:generate)
      end

      def cleanup!
        # Clean up notification files and directories
        ["lib", "sig"].each do |base|
          notifications_path = File.join(Config::BASE_PATH, base, "peddler/notifications")
          FileUtils.rm_rf(notifications_path) if Dir.exist?(notifications_path)
        end
      end

      def all_notifications
        @all_notifications ||= notification_schema_files.map { |file| new(file) }
      end

      def filtered_notifications(filter)
        all_notifications.select { |notification| notification.notification_name.match?(/#{filter}/i) }
      end

      def notification_schema_files
        Dir.glob(File.join(Config::BASE_PATH, "selling-partner-api-models/schemas/notifications/*.json"))
      end
    end

    def initialize(file_path)
      @file_path = file_path
      @schema = JSON.parse(File.read(file_path))
      resolve_root_ref!
      apply_schema_fixes!
    end

    def generate
      written_files = [] #: Array[String]
      all_types = [] #: Array[Type]

      # Generate nested types first
      nested_results = generate_nested_types!
      written_files.concat(nested_results[:files])
      all_types.concat(nested_results[:types])

      # Generate Payload and Notification as types
      payload_results = generate_payload_types!
      written_files.concat(payload_results[:files])
      all_types.concat(payload_results[:types])

      notification_result = generate_notification_type!
      written_files << notification_result[:file]
      all_types << notification_result[:type]

      # Generate main convenience file
      written_files << generate_main_file!

      # Generate consolidated RBS signature
      written_files << generate_rbs!(all_types)

      # Batch format all written files
      format_files(written_files)

      Generator.logger.info("Generated #{notification_name}")
    end

    # Extract notification name from filename (e.g., "AnyOfferChangedNotification.json" => "AnyOfferChanged")
    def notification_name
      base_name = File.basename(file_path, ".json")
      # Remove "Notification" suffix but keep version suffix (e.g., "_2023-12-13")
      # Convert dashes in version suffix to underscores for valid Ruby identifiers
      base_name.sub(/Notification$/, "").tr("-", "_")
    end

    # Get the actual payload key name from the schema (handles PascalCase vs camelCase)
    def payload_key_name
      return @payload_key_name if defined?(@payload_key_name)

      # Check if schema uses "payload" or "Payload"
      @payload_key_name = if schema.dig("properties", "payload")
        "payload"
      elsif schema.dig("properties", "Payload")
        "Payload"
      else
        "payload" # fallback
      end
    end

    # Get the actual notification object name within payload (handles casing variations)
    def notification_object_name
      return @notification_object_name if defined?(@notification_object_name)

      payload_schema = schema.dig("properties", "payload") || schema.dig("properties", "Payload")
      return @notification_object_name = nil unless payload_schema

      # If payload uses a $ref, resolve it
      if payload_schema["$ref"]
        ref_path = payload_schema["$ref"].sub("#/definitions/", "")
        payload_schema = schema.dig("definitions", ref_path)
        return @notification_object_name = nil unless payload_schema
      end

      # Try different casing conventions
      [
        "#{notification_name.camelize(:lower)}Notification", # feedProcessingFinishedNotification
        "#{notification_name.camelize}Notification", # FeedProcessingFinishedNotification
      ].each do |name|
        if payload_schema.dig("properties", name)
          @notification_object_name = name
          break
        end
      end

      @notification_object_name
    end

    # Class name for the notification (e.g., "AnyOfferChanged")
    def class_name
      notification_name.camelize
    end

    # Extract payload version from the schema examples or filename
    def payload_version
      # Try to get from examples first
      examples = schema.dig("examples")
      if examples&.first
        return examples.first["payloadVersion"] || examples.first["PayloadVersion"]
      end

      # Fall back to filename-based version (e.g., "_2023-12-13")
      if file_path =~ /_(\d{4}-\d{2}-\d{2})\.json$/
        matched = ::Regexp.last_match(1)
        return matched if matched
      end

      # Default to 1.0 if no version found
      "1.0"
    end

    # Properties from the wrapper level (notificationVersion, notificationType, etc.)
    def wrapper_properties
      sorted_properties(schema["properties"] || {})
    end

    # Required properties from the wrapper level
    def wrapper_required_properties
      schema["required"] || []
    end

    # Convert JSON Schema type to Ruby type
    def ruby_type_for(prop_def, for_comment: false)
      # Handle nil or non-hash prop_def
      return "Hash" unless prop_def.is_a?(Hash)

      # If property has a $ref, it's an object/Hash
      return "Hash" if prop_def["$ref"]

      case prop_def["type"]
      when "string"
        "String"
      when "integer"
        "Integer"
      when "number"
        "Float"
      when "boolean"
        for_comment ? "Boolean" : ":boolean"
      when "array"
        item_type = ruby_type_for(prop_def["items"] || {}, for_comment: for_comment)
        if for_comment
          "Array<#{item_type}>"
        else
          "[#{item_type}]"
        end
      when "object"
        "Hash"
      else
        # Default to untyped for unknown types
        for_comment ? "Object" : "Hash"
      end
    end

    # Generate attribute name (underscore the property name)
    def attribute_name_for(prop_name, prop_def)
      underscored = prop_name.underscore
      # For boolean attributes, strip is_ prefix for more idiomatic Ruby
      if prop_def && prop_def["type"] == "boolean"
        underscored.sub(/^is_/, "")
      else
        underscored
      end
    end

    # Format property comment with description (no leading spaces - template handles indentation)
    def format_property_comment(prop_def)
      return_type = ruby_type_for(prop_def, for_comment: true)
      if prop_def["description"] && !prop_def["description"].empty? && !generic_placeholder?(prop_def["description"])
        description = convert_html_links_to_yard(prop_def["description"])
        description = convert_doc_links_to_full_url(description)
        split_long_comment_line("@return [#{return_type}] #{description}", base_indent: 0)
      else
        "# @return [#{return_type}]"
      end
    end

    # Class description from schema (no leading spaces - template handles indentation)
    def class_description
      return unless schema["description"]
      return if generic_placeholder?(schema["description"])

      description = convert_html_links_to_yard(schema["description"])
      description = convert_doc_links_to_full_url(description)
      split_long_comment_line(description, base_indent: 0)
    end

    # Detect if this is Type A (nested) or Type B (flat) notification
    def notification_type
      return @notification_type if defined?(@notification_type)

      payload_schema = schema.dig("properties", "payload") || schema.dig("properties", "Payload")
      return @notification_type = :unknown unless payload_schema

      # If payload uses a $ref, resolve it
      if payload_schema["$ref"]
        ref_path = payload_schema["$ref"].sub("#/definitions/", "")
        payload_schema = schema.dig("definitions", ref_path)
        return @notification_type = :unknown unless payload_schema
      end

      # Type A: has nested notification object (e.g., feedProcessingFinishedNotification)
      [
        "#{notification_name.camelize(:lower)}Notification",
        "#{notification_name.camelize}Notification",
      ].each do |name|
        if payload_schema.dig("properties", name)
          @notification_type = :nested
          return @notification_type
        end
      end

      # Type B: flat payload
      @notification_type = :flat
    end

    # Get envelope properties (notificationVersion, notificationType, eventTime, etc.)
    def envelope_properties
      props = {} #: Hash[String, untyped]
      ["notificationVersion", "NotificationVersion"].each do |key|
        props[key] = schema.dig("properties", key) if schema.dig("properties", key)
      end
      ["notificationType", "NotificationType"].each do |key|
        props[key] = schema.dig("properties", key) if schema.dig("properties", key)
      end
      ["payloadVersion", "PayloadVersion"].each do |key|
        props[key] = schema.dig("properties", key) if schema.dig("properties", key)
      end
      ["eventTime", "EventTime"].each do |key|
        props[key] = schema.dig("properties", key) if schema.dig("properties", key)
      end
      ["notificationMetadata", "NotificationMetadata"].each do |key|
        props[key] = schema.dig("properties", key) if schema.dig("properties", key)
      end
      props
    end

    # Get required envelope properties
    def envelope_required_properties
      required = schema["required"] || []
      envelope_keys = envelope_properties.keys
      required.select { |r| envelope_keys.include?(r) }
    end

    # Get payload schema (the full payload object)
    def payload_schema
      schema.dig("properties", "payload") || schema.dig("properties", "Payload")
    end

    # Get payload properties based on notification type
    def payload_properties
      return {} unless payload_schema

      resolved_payload = payload_schema
      # Resolve $ref if present
      if resolved_payload&.dig("$ref")
        ref_path = resolved_payload["$ref"].sub("#/definitions/", "")
        resolved_payload = schema.dig("definitions", ref_path)
        return {} unless resolved_payload
      end

      if notification_type == :nested
        # Type A: extract the nested notification object properties
        [
          "#{notification_name.camelize(:lower)}Notification",
          "#{notification_name.camelize}Notification",
        ].each do |name|
          nested_obj = resolved_payload.dig("properties", name)
          next unless nested_obj

          # Resolve nested object $ref if present
          if nested_obj["$ref"]
            ref_path = nested_obj["$ref"].sub("#/definitions/", "")
            nested_obj = schema.dig("definitions", ref_path)
          end
          return sorted_properties(nested_obj["properties"] || {}) if nested_obj
        end
        {}
      else
        # Type B: use payload properties directly
        sorted_properties(resolved_payload["properties"] || {})
      end
    end

    # Get required payload properties based on notification type
    def payload_required_properties
      return [] unless payload_schema

      resolved_payload = payload_schema
      # Resolve $ref if present
      if resolved_payload&.dig("$ref")
        ref_path = resolved_payload["$ref"].sub("#/definitions/", "")
        resolved_payload = schema.dig("definitions", ref_path)
        return [] unless resolved_payload
      end

      if notification_type == :nested
        # Type A: get required from nested notification object
        [
          "#{notification_name.camelize(:lower)}Notification",
          "#{notification_name.camelize}Notification",
        ].each do |name|
          nested_obj = resolved_payload.dig("properties", name)
          next unless nested_obj

          # Resolve nested object $ref if present
          if nested_obj["$ref"]
            ref_path = nested_obj["$ref"].sub("#/definitions/", "")
            nested_obj = schema.dig("definitions", ref_path)
          end
          return nested_obj["required"] || [] if nested_obj
        end
        []
      else
        # Type B: use payload required directly
        resolved_payload["required"] || []
      end
    end

    # Replace inline object definitions with $ref pointers to extracted types
    def replace_inline_objects_with_refs(properties, extracted_types)
      result = {} #: Hash[String, untyped]
      properties.each do |prop_name, prop_def|
        # Check if this is a money-like object
        if money_like_object?(prop_def)
          result[prop_name] = {
            "$ref" => "#/definitions/Money",
            "description" => prop_def["description"],
          }.compact
          next
        end

        # Check if this property matches an extracted type name
        if prop_def["type"] == "object" && extracted_types.key?(prop_name)
          # Replace with $ref
          result[prop_name] = { "$ref" => "#/definitions/#{prop_name}" }
        elsif prop_def["type"] == "array" && prop_def["items"]
          # Handle array items - check if they're objects that match extracted types
          items = prop_def["items"]

          # Handle case where items is an array (non-standard format)
          items = items.first if items.is_a?(Array) && !items.empty?

          # Check if items are money-like objects
          if items.is_a?(Hash) && money_like_object?(items)
            result[prop_name] = prop_def.dup
            result[prop_name]["items"] = { "$ref" => "#/definitions/Money" }
            next
          end

          # Check if items are inline objects that match extracted types
          if items.is_a?(Hash) && (items["type"] == "object" || items["anyOf"])
            item_type_name = prop_name.singularize
            if extracted_types.key?(item_type_name)
              result[prop_name] = prop_def.dup
              result[prop_name]["items"] = { "$ref" => "#/definitions/#{item_type_name}" }
            else
              result[prop_name] = prop_def
            end
          else
            result[prop_name] = prop_def
          end
        else
          result[prop_name] = prop_def
        end
      end
      result
    end

    # Detect if an object looks like a Money type
    # Money objects have: amount/Amount (number) + currencyCode/CurrencyCode (string)
    def money_like_object?(node)
      return false unless node.is_a?(Hash) && node["type"] == "object"
      return false unless node["properties"]

      props = node["properties"]

      # Check for both camelCase and PascalCase variants
      has_amount = props.key?("Amount") || props.key?("amount") || props.key?("currencyAmount")
      has_currency = props.key?("CurrencyCode") || props.key?("currencyCode")

      return false unless has_amount && has_currency

      # Verify types
      amount_prop = props["Amount"] || props["amount"] || props["currencyAmount"]
      currency_prop = props["CurrencyCode"] || props["currencyCode"]

      amount_prop&.dig("type") == "number" && currency_prop&.dig("type") == "string"
    end

    private

    # Check if description is a generic placeholder from Amazon's schema generator
    def generic_placeholder?(description)
      return false unless description

      description.match?(/^An explanation about the purpose of this instance\.?$/i)
    end

    # Generate consolidated RBS signature for all notification types
    def generate_rbs!(all_types)
      api_name = "notifications/#{notification_name.underscore}"
      Generator::RBS::Types.new(api_name, all_types).generate
    end

    # Resolve root-level $ref or #ref if present
    # Some schemas (B2B, PricingHealth) use refs to shared notificationResponse
    def resolve_root_ref!
      ref = schema["$ref"] || schema["#ref"]
      return unless ref

      # Extract the definition name from the ref (e.g., "#/definitions/notificationResponse" => "notificationResponse")
      if ref.start_with?("#/definitions/")
        def_name = ref.sub("#/definitions/", "")
        resolved_schema = schema["definitions"]&.[](def_name)

        if resolved_schema
          # Merge the resolved schema while keeping definitions
          definitions = schema["definitions"]
          @schema = resolved_schema.merge("definitions" => definitions)
        end
      end
    end

    # Apply fixes for known Amazon schema bugs
    def apply_schema_fixes!
      # Fix: AnyOfferChanged has "NotificatonTionVersion" (typo) in schema
      # but real Amazon data uses "NotificationVersion" (correct)
      if notification_name == "AnyOfferChanged"
        props = schema["properties"]
        if props&.key?("NotificatonTionVersion")
          Generator.logger.warn("Fixing schema bug in #{notification_name}: NotificatonTionVersion -> NotificationVersion")
          props["NotificationVersion"] = props.delete("NotificatonTionVersion")
        end
      end
    end

    def sorted_properties(props)
      # No required/optional sorting for notifications - use natural order
      props.sort.to_h
    end

    def output_file_path
      File.join(
        Config::BASE_PATH,
        "lib/peddler/notifications/#{notification_name.underscore}.rb",
      )
    end

    def main_template
      File.read(Config.template_path("notification_main"))
    end

    # Build Payload type definition
    def payload_type_definition
      if notification_type == :nested
        notif_obj_name = notification_object_name
        raise "Missing notification object name for nested notification" unless notif_obj_name

        # For nested notifications, Payload wraps the nested notification object with a $ref
        {
          "type" => "object",
          "description" => "Notification payload that wraps nested notification data",
          "properties" => {
            notif_obj_name => {
              "$ref" => "#/definitions/#{notif_obj_name.camelize}",
            },
          },
          "required" => [notif_obj_name],
        }
      else
        # For flat notifications, Payload has properties directly
        {
          "type" => "object",
          "description" => "Notification payload data",
          "properties" => payload_properties,
          "required" => payload_required_properties,
        }
      end
    end

    # Build Notification type definition
    def notification_type_definition
      # Get payload property with its description from schema
      payload_property_def = schema.dig("properties", payload_key_name) || {}

      {
        "type" => "object",
        "description" => class_description || "Notification envelope with metadata",
        "properties" => envelope_properties.merge({
          payload_key_name => {
            "$ref" => "#/definitions/Payload",
            "description" => payload_property_def["description"],
          }.compact,
        }),
        "required" => envelope_required_properties + [payload_key_name],
      }
    end

    def generate_payload_types!
      written_files = [] #: Array[String]
      type_instances = [] #: Array[Type]

      # Build spec with all extracted types
      extractor = JsonSchemaExtractor.new(schema, notification_name)
      extracted_types = extractor.extract_types

      # For nested notifications, generate the nested notification object as a separate type
      if notification_type == :nested
        notif_obj_name = notification_object_name
        raise "Missing notification object name for nested notification" unless notif_obj_name

        # Check if this notification object was already extracted from schema definitions
        # If so, it was already generated in generate_nested_types! and we shouldn't duplicate it
        unless extracted_types.key?(notif_obj_name.camelize)
          # Replace inline object properties with $refs to extracted types
          properties_with_refs = replace_inline_objects_with_refs(payload_properties, extracted_types)

          nested_notif_def = {
            "type" => "object",
            "description" => "Nested notification data",
            "properties" => properties_with_refs,
            "required" => payload_required_properties,
          }

          # Generate the nested notification object type
          api_name = "notifications/#{notification_name.underscore}"
          nested_type = Type.new(notif_obj_name.camelize, nested_notif_def, api_name, { "definitions" => extracted_types })
          written_files << nested_type.generate
          type_instances << nested_type

          # Add it to extracted types for Payload to reference
          extracted_types[notif_obj_name.camelize] = nested_notif_def
        end
      end

      openapi_spec = {
        "definitions" => extracted_types,
      }

      api_name = "notifications/#{notification_name.underscore}"
      payload_type = Type.new("Payload", payload_type_definition, api_name, openapi_spec)
      written_files << payload_type.generate
      type_instances << payload_type

      { files: written_files, types: type_instances }
    end

    def generate_notification_type!
      # Build spec with Payload reference
      openapi_spec = {
        "definitions" => {
          "Payload" => payload_type_definition,
        },
      }

      api_name = "notifications/#{notification_name.underscore}"
      notification_type = Type.new("Notification", notification_type_definition, api_name, openapi_spec)
      file_path = notification_type.generate

      { file: file_path, type: notification_type }
    end

    def generate_main_file!
      content = ERB.new(main_template, trim_mode: "-").result(binding)
      write_file(output_file_path, content)
    end

    # Check if any types in this notification use Money
    def needs_money?
      extractor = JsonSchemaExtractor.new(schema, notification_name)
      extracted_types = extractor.extract_types

      # Check if any extracted type has a $ref to Money
      extracted_types.any? do |_type_name, type_def|
        type_def["properties"]&.any? do |_prop_name, prop_def|
          prop_def["$ref"] == "#/definitions/Money" ||
            (prop_def["items"].is_a?(Hash) && prop_def["items"]["$ref"] == "#/definitions/Money")
        end
      end
    end

    # Get list of all nested type file names for requiring
    def nested_type_files
      extractor = JsonSchemaExtractor.new(schema, notification_name)
      extracted_types = extractor.extract_types

      # Return list of underscored type names for requiring
      extracted_types.keys.map(&:underscore).sort
    end

    # Generate nested types using the Type generator
    # Returns hash with :files (array of file paths) and :types (array of Type instances)
    def generate_nested_types!
      extractor = JsonSchemaExtractor.new(schema, notification_name)
      extracted_types = extractor.extract_types

      # Convert to OpenAPI-style specification for Type generator
      openapi_spec = {
        "definitions" => extracted_types,
      }

      # Generate each nested type and collect written files and Type instances
      written_files = [] #: Array[String]
      type_instances = [] #: Array[Type]

      extracted_types.each do |type_name, type_def|
        # Use the existing Type generator with notification-specific path
        api_name = "notifications/#{notification_name.underscore}"
        type = Type.new(type_name, type_def, api_name, openapi_spec)
        written_files << type.generate
        type_instances << type
      end

      { files: written_files, types: type_instances }
    end
  end
end
