require "async"
require "async/semaphore"

# InsightsReport model
class InsightsReport < ApplicationRecord
  CONCURRENCY_COUNT = 100

  belongs_to :respondent_category
  belongs_to :analysis_report, optional: true
  has_many :interview_insights, dependent: :destroy
  has_many :insight_summaries, dependent: :destroy

  def respondents
    Respondent.where(id: respondent_ids)
  end

  def interview_questions
    InterviewQuestion.where(id: question_ids).order(:position)
  end

  def self.update_questions_status(question_id)
    Rails.logger.debug "Updating Question Status for insights reports. Question ID - #{question_id}"
    insight_reports = InsightsReport.where("? = ANY (question_ids)", question_id)
    AnalysisReport.where(id: insight_reports.collect(&:analysis_report_id)).update!(requires_insight_regeneration: true)
  end

  def self.generate_insights(respondent_category_id:, insights_report_id:, is_all_data:, system_task_id:)
    model_map = FeatureModelMap.find_by(feature_name: "QUERY_INSIGHT_GENERATION")
    return unless model_map

    task = SystemTask.find(system_task_id)
    respondent_category = RespondentCategory.find(respondent_category_id)
    insights_report = is_all_data ? nil : InsightsReport.find(insights_report_id)

    respondents = if is_all_data
                    respondent_category.respondents.includes(:transcript_sections)
                  else
                    Respondent.includes(:transcript_sections).where(id: insights_report.respondent_ids)
                  end

    interview_questions = if is_all_data
                            respondent_category.interview_questions.includes(:question_sections)
                          else
                            InterviewQuestion.includes(:question_sections).where(id: insights_report.question_ids)
                          end

    full_question_data = []
    sectioned_question_data = []

    # Build tuples so a single question becomes multiple work items if it has multiple sections
    interview_questions.each do |q|
      if q.question_sections.empty?
        full_question_data << { id: q.id, question: q.question }
      else
        q.question_sections.each do |qs|
          sectioned_question_data << { id: q.id, question: q.question, master_section_id: qs.master_section_id }
        end
      end
    end

    system_prompt = <<~SYSTEM_PROMPT
      You are an expert market research data extractor.

      Carefully follow these instructions:

      1. **Extract Relevant Sections**: You will receive a transcript enclosed in <transcript> tags, and a JSON array of questions/topics enclosed in <topics> tags. For each topic in the array, identify and isolate all sections of the transcript that are relevant.

      2. **Trim to Relevance**: Trim the beginning and end of the selected sections to ensure every part directly relates to the question. Always include the interviewer's prompting question to provide context.

      3. **Response Guidelines**:
         - **Verbatim Only**: Do not truncate, summarize, rephrase, or add to the text.
         - **Original Format**: Preserve original formatting and speaker indications perfectly.
         - **Split Mentions**: If a topic is discussed in multiple different parts of the transcript, extract all relevant blocks. Separate non-contiguous blocks with exactly "[...]"
         - **Missing Topics**: If a topic is not discussed at all in the transcript, return an empty string "" for that value.

      4. **Output Format**:
         You must output ONLY a valid JSON object. Do not include markdown formatting like ```json.
         The keys of the JSON object must be the exact topic strings provided.
         The values must be the verbatim extracted text.
    SYSTEM_PROMPT

    respondent_work_items = respondents.map do |respondent|
      transcript_text = if respondent.respondent_type == "transcript"
                          respondent.docx_transcript.verbose_to_text
                        else
                          respondent.whisper_transcript.verbose_to_text
                        end

      sections_data = respondent.transcript_sections.map { |ts| { master_section_id: ts.master_section_id, content: ts.content } }

      { respondent_id: respondent.id, transcript_text:, sections_data: }
    end

    respondent_semaphore = Async::Semaphore.new(15)
    section_semaphore = Async::Semaphore.new(20)

    Async do
      respondent_work_items.map do |ritem|
        Async do
          respondent_semaphore.acquire do
            tasks = []

            if full_question_data.any?
              tasks << Async do
                section_semaphore.acquire do
                  full_prompt = <<~PROMPT
                    <transcript>
                      #{ritem[:transcript_text]}
                    </transcript>
                    <topics>
                      #{full_question_data.map { |q| q[:question] }.to_json}
                    </topics>
                  PROMPT

                  chat = RubyLLM.chat(provider: model_map.provider, model: model_map.llm_model_name)
                  chat.with_instructions(system_prompt)
                  response = chat.ask(full_prompt)
                  json_match = response.content.match(/\{.*\}/m)

                  if json_match
                    begin
                      extracted_data_full = JSON.parse(json_match[0])
                      extracted_data_full.each do |question_text, transcript_extract|
                        question_record = full_question_data.find { |q| q[:question] == question_text }
                        next unless question_record

                        insight_record = InterviewInsight.find_or_initialize_by(
                          respondent_id: ritem[:respondent_id],
                          interview_question_id: question_record[:id],
                          insights_report_id: insights_report_id.presence,
                          master_section_id: nil
                        )
                        insight_record.update!(sources: transcript_extract)
                      end
                    rescue JSON::ParserError => e
                      Rails.logger.error("Failed to parse JSON for full transcript of Respondent #{ritem[:respondent_id]}: #{e.message}")
                    end
                  else
                    Rails.logger.error("No JSON found in LLM response for Respondent #{ritem[:respondent_id]} (Full Transcript)")
                  end
                end
              end
            end

            sectioned_question_data.each do |qdata|
              section = ritem[:sections_data].find { |s| s[:master_section_id] == qdata[:master_section_id] }
              section_content = section&.dig(:content)
              next if section_content.blank?

              tasks << Async do
                section_semaphore.acquire do
                  sectioned_prompt = <<~PROMPT
                    <transcript>
                      #{section_content}
                    </transcript>
                    <topics>
                      #{[qdata[:question]].to_json}
                    </topics>
                  PROMPT

                  chat = RubyLLM.chat(provider: model_map.provider, model: model_map.llm_model_name)
                  chat.with_instructions(system_prompt)
                  response = chat.ask(sectioned_prompt)
                  json_match = response.content.match(/\{.*\}/m)

                  if json_match
                    begin
                      extracted_data_sectioned = JSON.parse(json_match[0])
                      extracted_data_sectioned.each_value do |transcript_extract|
                        insight_record = InterviewInsight.find_or_initialize_by(
                          respondent_id: ritem[:respondent_id],
                          interview_question_id: qdata[:id],
                          insights_report_id: insights_report_id.presence,
                          master_section_id: qdata[:master_section_id]
                        )
                        insight_record.update!(sources: transcript_extract)
                      end
                    rescue JSON::ParserError => e
                      Rails.logger.error("Failed to parse JSON for sectioned question #{qdata[:id]} of Respondent #{ritem[:respondent_id]}: #{e.message}")
                    end
                  else
                    Rails.logger.error("No JSON found in LLM response for Respondent #{ritem[:respondent_id]} (Sectioned Question #{qdata[:id]})")
                  end
                end
              end
            end

            tasks.map(&:wait)
          end
        end
      end.map(&:wait)
    end

    task&.set_progress!(org_id: task.user.organization_id, progress: 35, metadata: {
                          groupable_id: is_all_data ? respondent_category_id : insights_report.analysis_report.id,
                          all_data: is_all_data,
                          report_type: "insight"
                        })

    InsightsReportSummaryJob.perform_later(
      metadata: {
        insights_report_id: insights_report&.id,
        respondent_category_id:,
        is_all_data:
      },
      user_id: task&.user_id
    )
  rescue StandardError => e
    Rails.logger.error("Critical Error generating insights - #{e.message}")
    Rails.logger.error(e.backtrace.join("\n"))
    task = SystemTask.find(system_task_id)
    task&.update(running: false, status: "error")
    SystemTask.execute_next_task!(task_class: task&.task_class)
  end

  def self.generate_summaries(respondent_category_id:, insights_report_id:, is_all_data:, system_task_id:)
    task = SystemTask.find(system_task_id)
    respondent_category = RespondentCategory.find(respondent_category_id)
    insights_report = is_all_data ? nil : InsightsReport.find(insights_report_id)

    respondents = if is_all_data
                    respondent_category.respondents.includes(:interview_insights)
                  else
                    Respondent.includes(:interview_insights).where(id: insights_report.respondent_ids)
                  end

    language = if is_all_data
                 respondent_category.has_homogenous_language_respondents ? respondent_category.respondents.pick(:language).to_sym : :en
               else
                 insights_report.language.to_sym rescue :en # rubocop:disable Style/RescueModifier
               end

    work_items = []
    respondents.each do |respondent|
      insights = if is_all_data
                   respondent.interview_insights.where(insights_report_id: nil)
                 else
                   respondent.interview_insights.where(insights_report_id:)
                 end

      insights.find_each do |interview_insight|
        next if interview_insight.sources.blank?

        work_items << { insight_id: interview_insight.id }
      end
    end

    semaphore = Async::Semaphore.new(CONCURRENCY_COUNT)

    Async do
      work_items.map do |item|
        Async do
          semaphore.acquire do
            insight = InterviewInsight.find(item[:insight_id])
            insight.ai_summarize(language:)
          end
        end
      end.map(&:wait)
    end

    task&.set_progress!(org_id: task.user.organization_id, progress: 70, metadata: {
                          groupable_id: is_all_data ? respondent_category_id : insights_report.analysis_report.id,
                          all_data: is_all_data,
                          report_type: "insight"
                        })
    generate_aggregate_insight_summaries(respondent_category_id:, insights_report_id:, is_all_data:, system_task_id:)
  rescue StandardError => e
    Rails.logger.error("Critical Error generating insight summaries - #{e.message}")
    Rails.logger.error(e.backtrace.join("\n"))
    task = SystemTask.find(system_task_id)
    task&.update(running: false, status: "error")
    SystemTask.execute_next_task!(task_class: task&.task_class)
  end

  def self.generate_aggregate_insight_summaries(respondent_category_id:, insights_report_id:, is_all_data:, system_task_id:)
    Rails.logger.fatal("**** GENERATING AGGREGATE INSIGHT SUMMARIES, ALL DATA -> #{is_all_data} ***** INSIGHTS REPORT -> #{insights_report_id} ****")
    task = SystemTask.find(system_task_id)
    respondent_category = RespondentCategory.find(respondent_category_id)
    insights_report = is_all_data ? nil : InsightsReport.find(insights_report_id)

    interview_questions = if is_all_data
                            respondent_category.interview_questions
                          else
                            InterviewQuestion.where(id: insights_report.question_ids)
                          end

    respondent_ids = if is_all_data
                       respondent_category.respondents.pluck(:id)
                     else
                       insights_report.respondent_ids
                     end

    language = if is_all_data
                 respondent_category.has_homogenous_language_respondents ? respondent_category.respondents.pick(:language).to_sym : :en
               else
                 insights_report.language.to_sym rescue :en # rubocop:disable Style/RescueModifier
               end

    instruction = Instruction.find_by(key: "AGGREGATE_SUMMARIES")
    model_map = FeatureModelMap.find_by(feature_name: "INSIGHT_SUMMARIES")
    language_prompt = " The language to be used for the summarization is #{Respondent::LANGUAGES.transform_values(&:titleize)[language]}"

    # 1. Fetch all valid insights across all target respondents/questions
    all_interview_insights = InterviewInsight.where(
      respondent_id: respondent_ids,
      insights_report_id: is_all_data ? nil : insights_report_id,
      interview_question_id: interview_questions.select(:id)
    ).where.not(ai_summary: nil)

    # 2. Group them by their unique Question + Section combination
    grouped_insights = all_interview_insights.group_by { |i| [i.interview_question_id, i.master_section_id] }

    # 3. Create work items that are tightly scoped to the specific section
    work_items = grouped_insights.filter_map do |(question_id, section_id), insights|
      next if insights.empty?

      summaries = insights.collect { |x| "<summary>#{x.ai_summary}</summary>" }.join("\n")

      {
        interview_question_id: question_id,
        master_section_id: section_id,
        summaries:,
        respondent_category_id:,
        insights_report_id: is_all_data ? nil : insights_report_id
      }
    end

    semaphore = Async::Semaphore.new(CONCURRENCY_COUNT)

    Async do
      work_items.map do |item|
        Async do
          semaphore.acquire do
            system_prompt = instruction.system_prompt.dup
            system_prompt.concat(language_prompt)
            prompt = "#{instruction.prompt} #{item[:summaries]}"

            # Rails.logger.debug prompt

            response = LanguageModel.send_with_system_prompt_v2(
              prompt, system_prompt,
              language_model: model_map.llm_model_name,
              provider: model_map.provider
            )

            next unless response[:status] == "ok"

            interview_question = InterviewQuestion.find(item[:interview_question_id])

            summary = if is_all_data
                        rc = RespondentCategory.find(item[:respondent_category_id])
                        InsightSummary.find_or_initialize_by(
                          interview_question:,
                          master_section_id: item[:master_section_id],
                          groupable: rc,
                          all_data: true
                        )
                      else
                        ir = InsightsReport.find(item[:insights_report_id])
                        InsightSummary.find_or_initialize_by(
                          interview_question:,
                          master_section_id: item[:master_section_id],
                          groupable: ir,
                          all_data: false
                        )
                      end

            summary.update!(summary: response[:data])
          end
        end
      end.map(&:wait)
    end

    task&.set_progress!(org_id: task.user.organization_id, progress: 100, metadata: {
                          groupable_id: is_all_data ? respondent_category_id : insights_report.analysis_report.id,
                          all_data: is_all_data,
                          report_type: "insight"
                        })
    respondent_category.update!(all_data_insight_report_generated: true, requires_insight_regeneration: false) if is_all_data
  rescue StandardError => e
    Rails.logger.error("Critical Error generating insight summaries - #{e.message}")
    Rails.logger.error(e.backtrace.join("\n"))
    task = SystemTask.find(system_task_id)
    task&.update(running: false, status: "error")
    SystemTask.execute_next_task!(task_class: task&.task_class)
  end

  def remove_respondent!(respondent_id)
    target_id = respondent_id.to_i
    respondent_ids.delete(target_id)
    save!
    analysis_report.update!(requires_insight_regeneration: true, requires_section_regeneration: true)
  end
end
