lib/attio/resources/note.rb



# frozen_string_literal: true

require_relative "../api_resource"

module Attio
  # Represents a note attached to a record in Attio
  class Note < APIResource
    api_operations :list, :retrieve, :create, :delete

    # API endpoint path for notes
    # @return [String] The API path
    def self.resource_path
      "notes"
    end

    # Read-only attributes - notes are immutable
    attr_reader :parent_object, :parent_record_id, :title, :format,
      :created_by_actor, :content_plaintext, :content_markdown, :tags, :metadata

    # Alias for compatibility
    alias_method :created_by, :created_by_actor

    # Convenience method to get content based on format
    def content
      case format
      when "plaintext"
        content_plaintext
      when "html", "markdown"
        content_markdown
      else
        content_plaintext
      end
    end

    def initialize(attributes = {}, opts = {})
      super
      normalized_attrs = normalize_attributes(attributes)
      @parent_object = normalized_attrs[:parent_object]
      @parent_record_id = normalized_attrs[:parent_record_id]
      @title = normalized_attrs[:title]
      @content_plaintext = normalized_attrs[:content_plaintext]
      @content_markdown = normalized_attrs[:content_markdown]
      @tags = normalized_attrs[:tags] || []
      @metadata = normalized_attrs[:metadata] || {}
      @format = normalized_attrs[:format] || "plaintext"
      @created_by_actor = normalized_attrs[:created_by_actor]
    end

    # Get the parent record
    def parent_record(**)
      return nil unless parent_object && parent_record_id

      Internal::Record.retrieve(
        object: parent_object,
        record_id: parent_record_id,
        **
      )
    end

    # Check if note is in HTML format
    def html?
      format == "html"
    end

    # Check if note is in plaintext format
    def plaintext?
      format == "plaintext"
    end

    # Get plaintext version of content
    def to_plaintext
      return content_plaintext if content_plaintext

      # If no plaintext, try to get markdown/html content and strip HTML
      html_content = content_markdown || content
      return nil unless html_content

      strip_html(html_content)
    end

    def resource_path
      raise InvalidRequestError, "Cannot generate path without an ID" unless persisted?
      note_id = id.is_a?(Hash) ? (id[:note_id] || id["note_id"]) : id
      "#{self.class.resource_path}/#{note_id}"
    end

    # Override destroy to handle nested ID
    def destroy(**opts)
      raise InvalidRequestError, "Cannot destroy a note without an ID" unless persisted?

      note_id = id.is_a?(Hash) ? (id[:note_id] || id["note_id"]) : id
      self.class.delete(note_id, **opts)
      freeze
      true
    end

    # Notes cannot be updated
    def save(*)
      raise NotImplementedError, "Notes cannot be updated. Create a new note instead."
    end

    def update(*)
      raise NotImplementedError, "Notes cannot be updated. Create a new note instead."
    end

    # Convert note to hash representation
    # @return [Hash] Note data as a hash
    def to_h
      super.merge(
        parent_object: parent_object,
        parent_record_id: parent_record_id,
        content: content,
        format: format,
        created_by_actor: created_by_actor,
        content_plaintext: content_plaintext
      ).compact
    end

    class << self
      # Override retrieve to handle nested ID
      def retrieve(id, **opts)
        note_id = id.is_a?(Hash) ? (id[:note_id] || id["note_id"]) : id
        validate_id!(note_id)
        response = execute_request(:GET, "#{resource_path}/#{note_id}", {}, opts)
        new(response["data"] || response, opts)
      end

      # Override create to handle validation and parameter mapping
      def create(**kwargs)
        # Extract options from kwargs
        opts = {}
        opts[:api_key] = kwargs.delete(:api_key) if kwargs.key?(:api_key)

        # Map object/record_id to parent_object/parent_record_id
        normalized_params = {
          parent_object: kwargs[:object] || kwargs[:parent_object],
          parent_record_id: kwargs[:record_id] || kwargs[:parent_record_id],
          title: kwargs[:title] || kwargs[:content] || "Note",
          content: kwargs[:content],
          format: kwargs[:format]
        }

        prepared_params = prepare_params_for_create(normalized_params)
        response = execute_request(:POST, resource_path, prepared_params, opts)
        new(response["data"] || response, opts)
      end

      # Override create to handle validation
      def prepare_params_for_create(params)
        validate_parent!(params[:parent_object], params[:parent_record_id])
        validate_content!(params[:content])
        validate_format!(params[:format]) if params[:format]

        {
          data: {
            title: params[:title],
            parent_object: params[:parent_object],
            parent_record_id: params[:parent_record_id],
            content: params[:content],
            format: params[:format] || "plaintext"
          }
        }
      end

      # Get notes for a record
      def for_record(params = {}, object:, record_id:, **)
        list(
          params.merge(
            parent_object: object,
            parent_record_id: record_id
          ),
          **
        )
      end

      private

      def validate_parent!(parent_object, parent_record_id)
        if parent_object.nil? || parent_object.to_s.empty?
          raise ArgumentError, "parent_object is required"
        end

        if parent_record_id.nil? || parent_record_id.to_s.empty?
          raise ArgumentError, "parent_record_id is required"
        end
      end

      def validate_content!(content)
        if content.nil? || content.to_s.strip.empty?
          raise ArgumentError, "content cannot be empty"
        end
      end

      def validate_format!(format)
        valid_formats = %w[plaintext html]
        unless valid_formats.include?(format.to_s)
          raise ArgumentError, "Invalid format: #{format}. Valid formats: #{valid_formats.join(", ")}"
        end
      end
    end

    private

    def strip_html(html)
      return html unless html.is_a?(String)

      # Basic HTML stripping (production apps should use a proper HTML parser)
      html.gsub(/<[^>]+>/, " ")
        .gsub(/\s+/, " ")
        .strip
    end
  end
end