# frozen_string_literal: true

require "securerandom"
require "json"
require_relative "observable"
require_relative "task_definition"
require_relative "agent_specification"

module Agentic
  # Represents an individual task to be executed by an agent
  # @attr_reader [String] id Unique identifier for the task
  # @attr_reader [String] description Human-readable description of the task
  # @attr_reader [Hash] agent_spec Requirements for the agent that will execute this task
  # @attr_reader [Hash] input Input data for the task
  # @attr_reader [Hash, nil] output Output produced by the task, nil if not yet executed
  # @attr_reader [Symbol] status Current status of the task (:pending, :in_progress, :completed, :failed)
  # @attr_reader [TaskFailure, nil] failure Failure information if the task failed, nil otherwise
  # @attr_reader [Boolean, nil] ready_to_execute Flag indicating if the task is ready to be executed
  # @attr_accessor [Integer, nil] retry_count Number of times the task has been retried
  # @attr_accessor [Symbol, nil] output_schema_name Name of the output schema to use
  class Task
    include Agentic::Observable

    attr_reader :id, :description, :agent_spec, :input, :output, :status, :failure, :ready_to_execute
    attr_accessor :retry_count, :output_schema_name

    # Initializes a new task
    # @param description [String] Human-readable description of the task
    # @param agent_spec [Hash, AgentSpecification] Requirements for the agent that will execute this task
    # @param input [Hash] Input data for the task
    # @param output_schema_name [Symbol, nil] Name of the output schema to use for structured output
    # @return [Task] A new task instance
    def initialize(description:, agent_spec:, input: {}, output_schema_name: nil)
      @id = SecureRandom.uuid
      @description = description

      # Convert agent_spec to AgentSpecification if it's a hash
      @agent_spec = if agent_spec.is_a?(Hash)
        AgentSpecification.new(
          name: agent_spec["name"],
          description: agent_spec["description"] || "",
          instructions: agent_spec["instructions"]
        )
      else
        agent_spec
      end

      @input = input
      @output = nil
      @failure = nil
      @status = :pending
      @ready_to_execute = nil
      @output_schema_name = output_schema_name
    end

    # Creates a task from a TaskDefinition
    # @param definition [TaskDefinition] The task definition
    # @param input [Hash] Input data for the task
    # @return [Task] A new task instance
    def self.from_definition(definition, input = {})
      new(
        description: definition.description,
        agent_spec: definition.agent,
        input: input
      )
    end

    # Executes the task using the given agent
    # @param agent [Agent] The agent that will execute this task
    # @return [TaskResult] The result of the task execution
    def perform(agent)
      old_status = @status
      @status = :in_progress

      notify_observers(:status_change, old_status, @status)

      begin
        @output = if has_output_schema?
          agent.execute_with_schema(build_prompt, output_schema)
        else
          agent.execute(build_prompt)
        end
        old_status = @status
        @status = :completed

        notify_observers(:status_change, old_status, @status)

        TaskResult.new(
          task_id: @id,
          success: true,
          output: @output
        )
      rescue => e
        @failure = TaskFailure.new(
          message: e.message,
          type: e.class.name,
          context: {
            backtrace: e.backtrace&.first(10),
            agent_id: agent.respond_to?(:id) ? agent.id : nil
          }
        )

        old_status = @status
        @status = :failed

        notify_observers(:status_change, old_status, @status)
        notify_observers(:failure_occurred, @failure)

        Agentic.logger.error("Task execution failed: #{e.message}")

        TaskResult.new(
          task_id: @id,
          success: false,
          failure: @failure
        )
      end
    end

    # Retries a failed task
    # @param agent [Agent] The agent that will execute this task
    # @return [TaskResult] The result of the task execution
    # @raise [StandardError] If the task is not in a failed state
    def retry(agent)
      raise "Cannot retry a task that is not in a failed state" unless @status == :failed

      old_status = @status
      @status = :retrying

      notify_observers(:status_change, old_status, @status)

      perform(agent)
    end

    # Returns a serializable representation of the task
    # @return [Hash] The task as a hash
    def to_h
      {
        id: @id,
        description: @description,
        agent_spec: @agent_spec.is_a?(AgentSpecification) ? @agent_spec.to_h : @agent_spec,
        input: @input,
        output: @output,
        status: @status,
        failure: @failure&.to_h
      }
    end

    # Returns the output schema for this task
    # @return [Agentic::StructuredOutputs::Schema, nil] The schema or nil if none specified
    def output_schema
      return nil unless @output_schema_name
      TaskOutputSchemas.get(@output_schema_name)
    end

    # Checks if this task has a structured output schema
    # @return [Boolean] True if task has an output schema
    def has_output_schema?
      !@output_schema_name.nil? && TaskOutputSchemas.exists?(@output_schema_name)
    end

    # Sets the output schema for this task
    # @param schema_name [Symbol] The name of the schema to use
    def set_output_schema(schema_name)
      @output_schema_name = schema_name
    end

    private

    # Builds the prompt to be sent to the agent
    # @return [String] The formatted prompt
    def build_prompt
      output_requirements = if has_output_schema?
        "Provide your response as a structured JSON object that follows the specified schema. Do not include any markdown formatting, code blocks, or additional text - just the raw JSON."
      else
        "Provide your response as valid JSON only. Do not wrap the JSON in markdown code blocks or any other formatting. Return raw JSON that can be parsed directly."
      end

      <<~PROMPT
        [System Instructions]
        #{agent_spec.instructions}
        
        [Task Description]
        #{description}
        
        [Input Parameters]
        #{JSON.pretty_generate(input)}
        
        [Output Requirements]
        #{output_requirements}
      PROMPT
    end
  end
end
