# frozen_string_literal: true

require_relative 'base'

module PluginConfig
  module Operations
    # Structural validation for a plugin directory: plugin.json shape,
    # marketplace registration/consistency, and skills/ layout.
    class Validate < Base
      def initialize(params)
        super
        @plugin_path = params[:plugin_path]
      end

      attr_reader :plugin_path

      def call
        return missing_plugin_path_result unless plugin_path && !plugin_path.to_s.strip.empty?

        load_state

        checks = {
          plugin_json: check_plugin_json,
          required_fields: check_required_fields,
          name_matches_directory: check_name_matches_directory,
          marketplace_entry: check_marketplace_entry,
          description_matches: check_description_matches,
          skill_md_present: check_skill_md_present,
          skills_array_matches: check_skills_array_matches,
          marketplace_source: check_marketplace_source
        }

        build_result(checks)
      end

      private

      # ─── State loading ──────────────────────────────────────────────────────

      def load_state
        @plugin_json = load_plugin_json
        @marketplace_data = read_json(marketplace_file_path)
        @marketplace_entry = find_marketplace_entry
      end

      # True when no --marketplace_file was given and the default marketplace
      # location can't be found. In this case marketplace-dependent checks are
      # skipped rather than failed, so the validator works against plugin
      # directories that aren't registered in any marketplace.json (e.g. when
      # invoked from a different repo). If --marketplace_file was explicitly
      # given, a missing/invalid file is still a hard failure.
      def marketplace_explicitly_given?
        !marketplace_file.to_s.strip.empty?
      end

      def skip_marketplace_checks?
        !marketplace_explicitly_given? && !File.exist?(default_marketplace_file)
      end

      def plugin_json_path
        File.join(plugin_path, '.claude-plugin', 'plugin.json')
      end

      def load_plugin_json
        return nil unless File.exist?(plugin_json_path)

        read_json(plugin_json_path)
      end

      def marketplace_file_path
        marketplace_file || default_marketplace_file
      end

      def default_marketplace_file
        File.join(File.dirname(File.expand_path(plugin_path)), '.claude-plugin', 'marketplace.json')
      end

      def find_marketplace_entry
        return nil unless @plugin_json && @marketplace_data

        (@marketplace_data['plugins'] || []).find { |p| p['name'] == @plugin_json['name'] }
      end

      def skills_root
        File.join(plugin_path, 'skills')
      end

      def skill_directories
        return [] unless Dir.exist?(skills_root)

        Dir.children(skills_root).select { |entry| File.directory?(File.join(skills_root, entry)) }.sort
      end

      # ─── Checks ─────────────────────────────────────────────────────────────

      def check_plugin_json
        path = plugin_json_path
        return fail_check("plugin.json not found at #{path}") unless File.exist?(path)
        return fail_check("plugin.json at #{path} contains invalid JSON") unless @plugin_json

        pass_check("plugin.json exists and is valid JSON at #{path}")
      end

      def check_required_fields
        return fail_check('Cannot check required fields: plugin.json is missing or invalid') unless @plugin_json

        missing = %w[name description].select { |key| blank?(@plugin_json[key]) }
        return fail_check("Missing or empty required field(s): #{missing.join(', ')}") unless missing.empty?

        pass_check('Required fields present: name, description')
      end

      def check_name_matches_directory
        return fail_check('Cannot check name: plugin.json is missing or invalid') unless @plugin_json

        expected = directory_basename
        actual = @plugin_json['name']
        return fail_check("plugin.json name #{actual.inspect} does not match directory basename #{expected.inspect}") unless actual == expected

        pass_check("plugin.json name '#{actual}' matches directory basename '#{expected}'")
      end

      def check_marketplace_entry
        if skip_marketplace_checks?
          return skip_check(
            "No --marketplace_file given and default location #{default_marketplace_file} not found; " \
            'skipping marketplace registration check'
          )
        end
        return fail_check('Cannot check marketplace entry: plugin.json is missing or invalid') unless @plugin_json
        return fail_check("Marketplace file not found at #{marketplace_file_path}") unless File.exist?(marketplace_file_path)
        return fail_check("Marketplace file at #{marketplace_file_path} contains invalid JSON") unless @marketplace_data
        return fail_check("No marketplace entry found for plugin '#{@plugin_json['name']}' in #{marketplace_file_path}") unless @marketplace_entry

        pass_check("Found marketplace entry for '#{@plugin_json['name']}' in #{marketplace_file_path}")
      end

      def check_description_matches
        if skip_marketplace_checks?
          return skip_check(
            "No --marketplace_file given and default location #{default_marketplace_file} not found; " \
            'skipping description consistency check'
          )
        end
        unless @plugin_json && @marketplace_entry
          return fail_check('Cannot check description: plugin.json or marketplace entry is unavailable')
        end

        plugin_description = @plugin_json['description']
        marketplace_description = @marketplace_entry['description']
        if plugin_description == marketplace_description
          pass_check('plugin.json description matches marketplace.json description byte-for-byte')
        else
          fail_check(
            "Description drift: plugin.json has #{plugin_description.inspect}, " \
            "marketplace.json has #{marketplace_description.inspect}"
          )
        end
      end

      def check_skill_md_present
        return pass_check('No skills/ directory present; nothing to check') unless Dir.exist?(skills_root)

        missing = skill_directories.reject { |dir| File.exist?(File.join(skills_root, dir, 'SKILL.md')) }
        return fail_check("Missing SKILL.md in skill directories: #{missing.join(', ')}") unless missing.empty?

        pass_check('All skill directories contain SKILL.md')
      end

      def check_skills_array_matches
        return fail_check('Cannot check skills array: plugin.json is missing or invalid') unless @plugin_json
        return pass_check('plugin.json declares no skills array; nothing to check') unless @plugin_json.key?('skills')

        declared = Array(@plugin_json['skills']).sort
        actual = skill_directories
        if declared == actual
          pass_check('plugin.json skills array matches skills/ directories')
        else
          fail_check("plugin.json skills array #{declared.inspect} does not match actual directories #{actual.inspect}")
        end
      end

      def check_marketplace_source
        if skip_marketplace_checks?
          return skip_check(
            "No --marketplace_file given and default location #{default_marketplace_file} not found; " \
            'skipping marketplace source check'
          )
        end
        return fail_check('Cannot check marketplace source: marketplace entry is unavailable') unless @marketplace_entry

        actual = @marketplace_entry['source']
        return fail_check('Marketplace entry has no source field') if blank?(actual)

        resolved = File.expand_path(actual, marketplace_root)
        expected = File.expand_path(plugin_path)
        if resolved == expected
          pass_check("Marketplace source '#{actual}' resolves to plugin directory")
        else
          fail_check(
            "Marketplace source #{actual.inspect} resolves to #{resolved.inspect}, " \
            "which does not match plugin directory #{expected.inspect}"
          )
        end
      end

      # The directory a marketplace entry's `source` is resolved relative to.
      # marketplace.json conventionally lives at <root>/.claude-plugin/marketplace.json,
      # with sources relative to <root>. If the marketplace file isn't nested under a
      # .claude-plugin/ directory (e.g. a flat fixture file), fall back to its own
      # containing directory.
      def marketplace_root
        dir = File.dirname(File.expand_path(marketplace_file_path))
        File.basename(dir) == '.claude-plugin' ? File.dirname(dir) : dir
      end

      # ─── Helpers ────────────────────────────────────────────────────────────

      def directory_basename
        File.basename(plugin_path.to_s.chomp('/'))
      end

      def blank?(value)
        value.nil? || (value.is_a?(String) && value.strip.empty?)
      end

      def pass_check(message)
        { status: 'pass', message: message }
      end

      def fail_check(message)
        { status: 'fail', message: message }
      end

      def skip_check(message)
        { status: 'skipped', message: message }
      end

      def build_result(checks)
        failed = checks.values.select { |check| check[:status] == 'fail' }

        {
          status: failed.empty? ? 'ok' : 'issues_found',
          valid: failed.empty?,
          plugin_path: plugin_path,
          checks: checks,
          issues: failed.map { |check| check[:message] }
        }
      end

      def missing_plugin_path_result
        {
          status: 'issues_found',
          valid: false,
          plugin_path: plugin_path,
          checks: {},
          issues: ['No plugin_path given. Usage: validate --plugin_path=<path-to-plugin>']
        }
      end
    end
  end
end
