"use client";

import { useQueryClient } from "@tanstack/react-query";
import {
  createContext,
  type ReactNode,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import type { QuestionField } from "@/data/question-data";
import { toBackendSlug } from "@/data/section-slug-map";
import { pathParam } from "@/hooks/marriage/path-param";
import { marriageQueryKeys } from "@/hooks/marriage/query-keys";
import type {
  MarriageField,
  MarriageFieldValue,
  MarriagePhoneFieldValue,
  UpdateMarriageSectionDataPayload,
} from "@/hooks/marriage/types";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import {
  useMarriageSectionDataQuery,
  useUpdateMarriageSectionDataMutation,
} from "@/hooks/marriage/use-section-data";
import { getApiRequestUrl } from "@/lib/http";

const STORAGE_VERSION = 1;

type QuestionAnswersByKey = Record<string, MarriageField>;

type StoredQuestionAnswers = {
  version: typeof STORAGE_VERSION;
  slug: string;
  current_step: number;
  fields: MarriageField[];
  pending_sync: boolean;
  updated_at: string;
};

type FlushAnswersOptions = {
  force?: boolean;
};

type QuestionAnswersContextValue = {
  flushAnswers: (options?: FlushAnswersOptions) => Promise<void>;
  getAnswerValue: (
    question: QuestionField,
    questionIndex: number,
  ) => MarriageFieldValue | undefined;
  hasPendingSync: boolean;
  isSaving: boolean;
  isLoading: boolean;
  setAnswerValue: (
    question: QuestionField,
    questionIndex: number,
    value: MarriageFieldValue,
  ) => void;
};

type QuestionAnswersProviderProps = {
  children: ReactNode;
  questions: readonly QuestionField[];
  slug: string;
};

const QuestionAnswersContext =
  createContext<QuestionAnswersContextValue | null>(null);

function hashString(value: string) {
  let hash = 0;

  for (let index = 0; index < value.length; index += 1) {
    hash = (hash * 31 + value.charCodeAt(index)) >>> 0;
  }

  return hash.toString(36);
}

function slugifyQuestionTitle(title: string) {
  const slug = title
    .normalize("NFKD")
    .replace(/[\u0300-\u036f]/g, "")
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "_")
    .replace(/^_+|_+$/g, "");

  return slug || `field_${hashString(title)}`;
}

function getQuestionFieldKey(question: QuestionField, questionIndex: number) {
  const index =
    question.originalIndex !== undefined
      ? question.originalIndex
      : questionIndex;
  return `q${index + 1}_${slugifyQuestionTitle(question.englishTitle || question.title)}`;
}

export function getQuestionAnswersStorageKey(slug: string) {
  return `marriage:sections:${slug}:answers`;
}

export function hasQuestionAnswerValue(value: MarriageFieldValue) {
  if (value === null) {
    return false;
  }

  if (typeof value === "string") {
    return value.trim().length > 0;
  }

  return true;
}

function isMarriageField(value: unknown): value is MarriageField {
  if (!value || typeof value !== "object") {
    return false;
  }

  const field = value as Partial<MarriageField>;

  return (
    typeof field.key === "string" &&
    typeof field.label === "string" &&
    typeof field.type === "string" &&
    (field.value === null ||
      typeof field.value === "string" ||
      typeof field.value === "number" ||
      typeof field.value === "boolean" ||
      Array.isArray(field.value) ||
      isMarriagePhoneFieldValue(field.value))
  );
}

function isMarriagePhoneFieldValue(
  value: unknown,
): value is MarriagePhoneFieldValue {
  if (!value || typeof value !== "object") {
    return false;
  }

  const phoneValue = value as Partial<MarriagePhoneFieldValue>;

  return (
    typeof phoneValue.countryCode === "string" &&
    typeof phoneValue.phoneNumber === "string"
  );
}

function createQuestionField(
  question: QuestionField,
  questionIndex: number,
  value: MarriageFieldValue,
): MarriageField {
  return {
    key: getQuestionFieldKey(question, questionIndex),
    label: question.title,
    type: question.type,
    value,
    private: question.private,
  };
}

function getOrderedFields(
  answers: QuestionAnswersByKey,
  questions: readonly QuestionField[],
) {
  const orderedFields: MarriageField[] = [];
  const orderedKeys = new Set<string>();

  questions.forEach((question, index) => {
    const key = getQuestionFieldKey(question, index);
    const field = answers[key];

    if (field) {
      orderedFields.push(field);
      orderedKeys.add(key);
    }
  });

  Object.entries(answers).forEach(([key, field]) => {
    if (!orderedKeys.has(key)) {
      orderedFields.push(field);
    }
  });

  return orderedFields;
}

function getCurrentStep(
  fields: MarriageField[],
  questions: readonly QuestionField[],
) {
  return questions.filter((question, index) => {
    if (!question.required || question.logic?.dependsOn) {
      return false;
    }
    const key = getQuestionFieldKey(question, index);
    const field = fields.find((f) => f.key === key);
    return field && hasQuestionAnswerValue(field.value);
  }).length;
}

function createPayload(
  answers: QuestionAnswersByKey,
  questions: readonly QuestionField[],
): UpdateMarriageSectionDataPayload {
  const fields = getOrderedFields(answers, questions);
  const targetQuestions = questions.filter(
    (q) => q.required && !q.logic?.dependsOn,
  );

  return {
    current_step: getCurrentStep(fields, questions),
    total_steps: targetQuestions.length,
    fields,
  };
}

function fieldsToAnswers(fields: MarriageField[]) {
  return fields.reduce<QuestionAnswersByKey>((nextAnswers, field) => {
    nextAnswers[field.key] = field;
    return nextAnswers;
  }, {});
}

function readStoredAnswers(storageKey: string, slug: string) {
  try {
    const rawValue = window.localStorage.getItem(storageKey);

    if (!rawValue) {
      return {
        answers: {},
        pendingSync: false,
      };
    }

    const storedValue = JSON.parse(rawValue) as Partial<StoredQuestionAnswers>;
    const fields = Array.isArray(storedValue.fields)
      ? storedValue.fields.filter(isMarriageField)
      : [];

    return {
      answers: fieldsToAnswers(fields),
      pendingSync:
        storedValue.slug === slug &&
        (storedValue.pending_sync ?? fields.length > 0),
    };
  } catch {
    return {
      answers: {},
      pendingSync: false,
    };
  }
}

function writeStoredAnswers(
  storageKey: string,
  slug: string,
  questions: readonly QuestionField[],
  answers: QuestionAnswersByKey,
  pendingSync: boolean,
) {
  try {
    const payload = createPayload(answers, questions);

    if (payload.fields.length === 0) {
      window.localStorage.removeItem(storageKey);
      return;
    }

    const storedValue: StoredQuestionAnswers = {
      version: STORAGE_VERSION,
      slug,
      current_step: payload.current_step,
      fields: payload.fields,
      pending_sync: pendingSync,
      updated_at: new Date().toISOString(),
    };

    window.localStorage.setItem(storageKey, JSON.stringify(storedValue));
  } catch {
    // localStorage can fail in private mode or when storage quota is exhausted.
  }
}

function getKeepalivePatchUrl(slug: string) {
  const backendSlug = toBackendSlug(slug);
  return getApiRequestUrl(
    `/api/marriage/sections/${pathParam(backendSlug)}/data/`,
  );
}

function getCsrfToken() {
  if (typeof document === "undefined") return "";
  const match = document.cookie.match(/(^|;)\s*csrftoken\s*=\s*([^;]+)/);
  return match ? match[2] : "";
}

export function QuestionAnswersProvider({
  children,
  questions,
  slug,
}: QuestionAnswersProviderProps) {
  const storageKey = useMemo(() => getQuestionAnswersStorageKey(slug), [slug]);
  const queryClient = useQueryClient();
  const [answers, setAnswers] = useState<QuestionAnswersByKey>({});
  const [hasPendingSync, setHasPendingSync] = useState(false);
  const { isPending: isSaving, mutateAsync } =
    useUpdateMarriageSectionDataMutation(slug);
  const answersRef = useRef<QuestionAnswersByKey>({});
  const hasPendingSyncRef = useRef(false);
  const answersRevisionRef = useRef(0);
  const flushPromiseRef = useRef<Promise<void> | null>(null);
  const questionsRef = useRef(questions);
  const storageKeyRef = useRef(storageKey);
  const slugRef = useRef(slug);

  const { data: profile } = useMarriageProfileQuery();
  const canEdit = profile?.can_edit_profile !== false;

  const { data: serverSectionData, isLoading: isLoadingData } =
    useMarriageSectionDataQuery(slug);

  useEffect(() => {
    questionsRef.current = questions;
  }, [questions]);

  useEffect(() => {
    storageKeyRef.current = storageKey;
    slugRef.current = slug;

    const stored = readStoredAnswers(storageKey, slug);
    let finalAnswers = stored.answers;
    let finalPendingSync = stored.pendingSync;

    if (serverSectionData?.data) {
      const serverAnswers = fieldsToAnswers(serverSectionData.data);
      if (stored.pendingSync && canEdit) {
        // Merge: local answers override server answers for unsynced changes
        finalAnswers = { ...serverAnswers, ...stored.answers };
      } else {
        // A section response can temporarily omit fields (most notably while
        // the combined family section is being refreshed). Keep locally known
        // fields that the response did not include, while letting explicit
        // server values, including null/empty values, win for matching keys.
        finalAnswers = { ...stored.answers, ...serverAnswers };
        finalPendingSync = false;
      }
    }

    answersRef.current = finalAnswers;
    hasPendingSyncRef.current = finalPendingSync;
    setAnswers(finalAnswers);
    setHasPendingSync(finalPendingSync);

    // Update localStorage to stay in sync
    writeStoredAnswers(
      storageKey,
      slug,
      questions,
      finalAnswers,
      finalPendingSync,
    );
  }, [slug, storageKey, serverSectionData, questions, canEdit]);

  const syncTimeoutRef = useRef<NodeJS.Timeout | null>(null);

  useEffect(() => {
    return () => {
      if (syncTimeoutRef.current !== null) {
        clearTimeout(syncTimeoutRef.current);
      }
    };
  }, []);

  const getAnswerValue = useCallback(
    (question: QuestionField, questionIndex: number) =>
      answers[getQuestionFieldKey(question, questionIndex)]?.value,
    [answers],
  );

  const setAnswerValue = useCallback(
    (
      question: QuestionField,
      questionIndex: number,
      value: MarriageFieldValue,
    ) => {
      if (!canEdit) {
        return;
      }
      const field = createQuestionField(question, questionIndex, value);

      setAnswers((currentAnswers) => {
        const nextAnswers = {
          ...currentAnswers,
          [field.key]: field,
        };

        answersRef.current = nextAnswers;
        hasPendingSyncRef.current = true;
        answersRevisionRef.current += 1;
        writeStoredAnswers(
          storageKeyRef.current,
          slugRef.current,
          questionsRef.current,
          nextAnswers,
          true,
        );

        if (syncTimeoutRef.current !== null) {
          clearTimeout(syncTimeoutRef.current);
        }

        const isTextLike =
          question.type === "text" ||
          question.type === "textarea" ||
          question.type === "number";

        const delay = isTextLike ? 1000 : 0;

        syncTimeoutRef.current = setTimeout(() => {
          void flushAnswersRef.current();
        }, delay);

        return nextAnswers;
      });
      setHasPendingSync(true);
    },
    [canEdit],
  );

  const flushAnswers = useCallback(
    async (options?: FlushAnswersOptions) => {
      if (!canEdit) {
        return;
      }

      if (flushPromiseRef.current) {
        await flushPromiseRef.current;
      }

      if (!hasPendingSyncRef.current && !options?.force) {
        return;
      }

      const payload = createPayload(answersRef.current, questionsRef.current);
      const revision = answersRevisionRef.current;

      if (payload.fields.length === 0) {
        return;
      }

      const request = mutateAsync(payload).then(() => undefined);
      flushPromiseRef.current = request;

      try {
        await request;
      } finally {
        if (flushPromiseRef.current === request) {
          flushPromiseRef.current = null;
        }
      }

      // Do not mark a newer edit as synced just because an older request
      // completed. A forced exit waits for and saves that newer revision too.
      if (revision !== answersRevisionRef.current) {
        if (options?.force) {
          await flushAnswersRef.current();
        }
        return;
      }

      hasPendingSyncRef.current = false;
      setHasPendingSync(false);
      writeStoredAnswers(
        storageKeyRef.current,
        slugRef.current,
        questionsRef.current,
        answersRef.current,
        false,
      );
    },
    [mutateAsync, canEdit],
  );
  const flushAnswersRef = useRef(flushAnswers);

  useEffect(() => {
    flushAnswersRef.current = flushAnswers;
  }, [flushAnswers]);

  useEffect(() => {
    const flushWithKeepalive = () => {
      if (!canEdit) {
        return;
      }
      if (!hasPendingSyncRef.current) {
        return;
      }

      // The combined family card must be split across two backend endpoints by
      // updateMarriageSectionData. Sending its full payload to either endpoint
      // would overwrite the other half of the profile. The local pending draft
      // remains available and is retried on the next visit.
      if (
        slugRef.current === "family_marital_history" ||
        flushPromiseRef.current
      ) {
        return;
      }

      const payload = createPayload(answersRef.current, questionsRef.current);
      const revision = answersRevisionRef.current;

      if (payload.fields.length === 0) {
        return;
      }

      const headers: Record<string, string> = {
        Accept: "application/json",
        "Content-Type": "application/json",
      };
      const csrfToken = getCsrfToken();
      if (csrfToken) {
        headers["X-CSRFToken"] = csrfToken;
      }

      fetch(getKeepalivePatchUrl(slugRef.current), {
        body: JSON.stringify(payload),
        credentials: "include",
        headers,
        keepalive: true,
        method: "PATCH",
      })
        .then((response) => {
          if (!response.ok) {
            return;
          }

          if (revision !== answersRevisionRef.current) {
            return;
          }

          hasPendingSyncRef.current = false;
          setHasPendingSync(false);
          writeStoredAnswers(
            storageKeyRef.current,
            slugRef.current,
            questionsRef.current,
            answersRef.current,
            false,
          );

          void queryClient.invalidateQueries({
            queryKey: marriageQueryKeys.profile(),
          });
          void queryClient.invalidateQueries({
            queryKey: marriageQueryKeys.sections(),
          });
          void queryClient.invalidateQueries({
            queryKey: marriageQueryKeys.sectionData(slugRef.current),
          });
        })
        .catch(() => {
          // The local draft stays marked pending so a later exit can retry.
        });
    };

    window.addEventListener("pagehide", flushWithKeepalive);

    return () => {
      window.removeEventListener("pagehide", flushWithKeepalive);
      flushWithKeepalive();
    };
  }, [canEdit, queryClient.invalidateQueries]);

  const contextValue = useMemo<QuestionAnswersContextValue>(
    () => ({
      flushAnswers,
      getAnswerValue,
      hasPendingSync,
      isSaving,
      isLoading: isLoadingData,
      setAnswerValue,
    }),
    [
      flushAnswers,
      getAnswerValue,
      hasPendingSync,
      isSaving,
      isLoadingData,
      setAnswerValue,
    ],
  );

  return (
    <QuestionAnswersContext.Provider value={contextValue}>
      {children}
    </QuestionAnswersContext.Provider>
  );
}

export function useQuestionAnswers() {
  const context = useContext(QuestionAnswersContext);

  if (!context) {
    throw new Error(
      "useQuestionAnswers must be used inside QuestionAnswersProvider",
    );
  }

  return context;
}

export function useQuestionAnswer(
  question: QuestionField,
  questionIndex: number,
) {
  const context = useContext(QuestionAnswersContext);

  return {
    setValue: (value: MarriageFieldValue) => {
      context?.setAnswerValue(question, questionIndex, value);
    },
    value: context?.getAnswerValue(question, questionIndex),
  };
}
