import { isEmpty } from 'lodash';
import {
  ReturnInfo,
  ReturnProcessAnswers,
  ReturnProcessQuestion,
  ReturnProcessQuestionKey,
  ReturnProcessQuestionType,
} from '../../models';
import { Product } from '../../schemas/product.schema';
import {
  activeReturnProcessQuestions,
  internalActiveReturnProcessQuestions,
} from './active-return-process-questions.helper';
import { allReturnProcessQuestionsAnswered } from './all-return-process-questions-answered.helper';
import { ReturnProcessQuestionSchema } from '../../questions';
import { ReturnProcessChecklistAnswerSchema } from '../../schemas';

/**
 * Union type for parameters accepted by getReturnInfo.
 * Provides flexibility to work with either the full question set or just active questions
 * to optimize performance in different scenarios.
 */
export type GetReturnInfoParams =
  | {
      questions: ReturnProcessQuestion[];
      answers: ReturnProcessAnswers;
    }
  | {
      activeQuestions: ReturnProcessQuestion[];
      answers: ReturnProcessAnswers;
    };

/**
 * Processes return process questions and answers to generate consolidated return information.
 * This function aggregates information from various questions and their answers to create
 * a comprehensive return info object.
 *
 * Features:
 * - Different question type handling (Select, Product)
 * - Answer validation using Zod schemas
 * - Circular dependency detection
 * - Aggregation of return information from multiple answers
 * - Support for both full and filtered question sets
 *
 * @param params - Object containing either full questions or active questions, and their answers
 * @returns Consolidated ReturnInfo object or undefined if:
 * - No answers are provided
 * - No questions are available
 * - Questions are incomplete
 * - A cyclic dependency is detected
 *
 * @throws {Error} If a cyclic dependency is detected in the question flow
 * @throws {Error} If an unsupported question type is encountered
 *
 * @example
 * ```typescript
 * const returnInfo = getReturnInfo({
 *   questions: returnProcessQuestions,
 *   answers: {
 *     condition: 'damaged',
 *     reason: 'defective'
 *   }
 * });
 * ```
 */
export function getReturnInfo(
  params: GetReturnInfoParams,
): ReturnInfo | undefined {
  if (isEmpty(params.answers)) {
    return undefined;
  }
  const { answers } = params;
  let activeQuestions: ReturnProcessQuestion[] = [];

  if ('questions' in params && Array.isArray(params.questions)) {
    activeQuestions = activeReturnProcessQuestions(params.questions, answers);
  } else if (
    'activeQuestions' in params &&
    Array.isArray(params.activeQuestions)
  ) {
    activeQuestions = params.activeQuestions;
  }

  if (activeQuestions.length === 0) {
    return undefined;
  }

  if (!allReturnProcessQuestionsAnswered({ activeQuestions, answers })) {
    return undefined;
  }

  const returnInfo: ReturnInfo = {};

  let currentQuestionKey: ReturnProcessQuestionKey | undefined =
    activeQuestions[0].key;
  const circleCheck = new Set<string>();

  while (currentQuestionKey) {
    if (circleCheck.has(currentQuestionKey)) {
      throw new Error('Cyclic question dependency detected');
    }
    circleCheck.add(currentQuestionKey);

    const question = activeQuestions.find((q) => q.key === currentQuestionKey);
    if (!question) {
      break;
    }

    const answer = answers[question.key];

    switch (question.type) {
      case ReturnProcessQuestionType.Select: {
        const selectedOption = question.options.find((o) => o.value === answer);
        if (selectedOption?.returnInfo) {
          assignReturnInfo(returnInfo, selectedOption.returnInfo);
        }
        currentQuestionKey = selectedOption?.nextQuestion;
        break;
      }
      case ReturnProcessQuestionType.Product: {
        const parseResult = ReturnProcessQuestionSchema[
          ReturnProcessQuestionKey.DeliveredItem
        ]
          .passthrough()
          .safeParse(answer);

        if (parseResult.success) {
          assignReturnInfo(returnInfo, { otherProduct: answer as Product });
        }

        currentQuestionKey = question.nextQuestion;
        break;
      }
      case ReturnProcessQuestionType.Checklist: {
        const parseResult =
          ReturnProcessChecklistAnswerSchema.passthrough().safeParse(answer);

        if (parseResult.success) {
          const selectedValues: string[] = [...parseResult.data.options];

          if (parseResult.data.other) {
            selectedValues.push(parseResult.data.other);
          }

          assignReturnInfo(returnInfo, {
            returnDetails: {
              [question.key]: selectedValues,
            },
          });
        }

        currentQuestionKey = question.nextQuestion;
        break;
      }
      case ReturnProcessQuestionType.Info: {
        currentQuestionKey = question.nextQuestion;
        break;
      }
      case ReturnProcessQuestionType.Group: {
        const f = internalActiveReturnProcessQuestions(
          question.questions,
          answers,
        );

        const groupReturnInfo = getReturnInfo({
          questions: f.questions,
          answers,
        });
        if (groupReturnInfo) {
          assignReturnInfo(returnInfo, groupReturnInfo);
        }

        currentQuestionKey = f.nextQuestion as ReturnProcessQuestionKey;
        break;
      }
      default: {
        throw new Error(`Unsupported question type`);
      }
    }
  }

  return returnInfo;
}

/**
 * Assigns all properties from `source` into `target`, merging `returnDetails` specially.
 *
 * @param target - The object to receive assigned properties.
 * @param source - The object providing properties to assign.
 *
 * @remarks
 * - If `source.returnDetails` is `undefined`, performs a straight shallow merge of all fields.
 * - Otherwise:
 *   1. Merges `returnDetails` by combining any existing entries in `target.returnDetails` with
 *      those from `source.returnDetails`.
 *   2. Shallow-assigns all other properties from `source` into `target`, leaving `returnDetails`
 *      untouched (since it was already merged).
 */
function assignReturnInfo(target: ReturnInfo, source: ReturnInfo) {
  // 1) If there are no returnDetails in source, copy everything at once.
  if (!source.returnDetails) {
    Object.assign(target, source);
    return;
  }

  // 2) Merge returnDetails:
  //    - Keep existing entries in target.returnDetails (if any)
  //    - Override/add entries from source.returnDetails
  target.returnDetails = {
    ...(target.returnDetails ?? {}),
    ...source.returnDetails,
  };

  // 3) Prepare all other fields (excluding returnDetails) for assignment
  const otherFields: Partial<ReturnInfo> = { ...source };
  delete otherFields.returnDetails;

  // 4) Shallow-merge the rest of the properties into target
  Object.assign(target, otherFields);
}
