class Whatsapp::CallService
  pattr_initialize [:call!, :agent!, :sdp_answer]

  def accept
    raise Voice::CallErrors::CallFailed, 'sdp_answer is required' if sdp_answer.blank?

    # All side effects under the lock so a concurrent terminate cannot finalize
    # the call between status update and the message/conversation/broadcast writes.
    call.with_lock do
      transition_to_in_progress!
      update_message_status('in_progress')
      claim_conversation_and_set_call_status
      broadcast(:accepted, accepted_by_agent_id: agent.id)
    end
    call
  end

  def reject
    call.with_lock do
      next if call.terminal? || call.in_progress?

      invoke_provider!(:reject_call)
      call.update!(accepted_by_agent_id: agent.id) if call.accepted_by_agent_id.nil?
      finalize_call('rejected', end_reason: 'agent_rejected')
    end
    call
  end

  def terminate
    call.with_lock do
      next if call.terminal?

      invoke_provider!(:terminate_call)
      # Compute duration from started_at locally — the webhook arrives after the
      # call is already terminal and the idempotency guard there bails before it
      # can fill these fields, so we have to record them here.
      if call.in_progress?
        duration = call.started_at ? (Time.current - call.started_at).to_i : nil
        finalize_call('completed', duration_seconds: duration, end_reason: 'agent_hangup')
      else
        # Agent hangs up before contact picks up → no_answer; mirrors the webhook terminate path.
        finalize_call('no_answer', end_reason: 'agent_hangup')
      end
    end
    call
  end

  private

  def transition_to_in_progress!
    # in_progress and terminal both make ringing? false; branch in order to surface the
    # distinct AlreadyAccepted / CallAlreadyEnded states (caller can hang up mid-ring).
    raise Voice::CallErrors::AlreadyAccepted, 'Call already accepted by another agent' if call.in_progress?
    raise Voice::CallErrors::CallAlreadyEnded, 'Call already ended' if call.terminal?
    raise Voice::CallErrors::NotRinging, 'Call is not in ringing state' unless call.ringing?

    forward_answer_to_meta!
    call.update!(status: 'in_progress', accepted_by_agent_id: agent.id, started_at: Time.current,
                 meta: (call.meta || {}).merge('sdp_answer' => sdp_answer))
  end

  def forward_answer_to_meta!
    invoke_provider!(:pre_accept_call, sdp_answer)
    invoke_provider!(:accept_call, sdp_answer)
  end

  # Claim an unheld conversation and set call_status in one save so previous_changes carries both the
  # assignee change (activity message + ASSIGNEE_CHANGED) and the call_status change (conversation.updated webhook).
  def claim_conversation_and_set_call_status
    conversation = call.conversation
    attrs = { additional_attributes: (conversation.additional_attributes || {}).merge('call_status' => call.display_status) }
    attrs[:assignee] = agent if conversation.assignee_id.blank?
    conversation.update!(attrs)
  end

  # Raise on Meta failure (bool false or transport error) so callers bail before
  # finalizing local state — otherwise we'd mark a still-active call as ended
  # and broadcast voice_call.ended while Meta thinks it's live.
  def invoke_provider!(method, *)
    success = call.inbox.channel.provider_service.public_send(method, call.provider_call_id, *)
    raise Voice::CallErrors::CallFailed, "Meta #{method} failed" unless success
  rescue Voice::CallErrors::CallFailed
    raise
  rescue StandardError => e
    Rails.logger.error "[WHATSAPP CALL] #{method} failed: #{e.class} #{e.message}"
    raise Voice::CallErrors::CallFailed, "Meta #{method} failed"
  end

  def finalize_call(status, **attrs)
    meta = (call.meta || {}).merge('ended_at' => Time.zone.now.to_i)
    call.update!(status: status, meta: meta, **attrs)
    update_message_status(status, duration_seconds: attrs[:duration_seconds])
    update_conversation_call_status(call.display_status)
    broadcast(:ended, status: call.display_status)
  end

  def update_message_status(status, duration_seconds: nil)
    Voice::CallMessageBuilder.new(call).update_status!(status: status, agent: agent, duration_seconds: duration_seconds)
  end

  def update_conversation_call_status(status)
    call.conversation.update!(
      additional_attributes: (call.conversation.additional_attributes || {}).merge('call_status' => status)
    )
  end

  def broadcast(event, **extra)
    payload = {
      event: "voice_call.#{event}",
      data: { id: call.id, call_id: call.provider_call_id, provider: call.provider,
              conversation_id: call.conversation_id, account_id: call.account_id }.merge(extra)
    }
    ActionCable.server.broadcast("account_#{call.account_id}", payload)
  end
end
