import fs from "node:fs/promises";
import path from "node:path";
import OpenAI from "openai";
import { fileURLToPath } from "node:url";
import { OPENAI_MODEL } from "../config.js";

const DIMENSION_KEYS = ["career", "health", "learning", "happiness", "contribution"];
const DEFAULT_CAREER_SLOPE_CAP = 0.85;
const CATEGORY_MAX_SCORES = {
  career: 40,
  learning: 20,
  health: 20,
  happiness: 10,
  contribution: 9,
};

const LEGACY_QUESTION_ID_MAP = {
  Ca1: "C1",
  Ca2: "C2",
  Ca3: "C3",
  He1: "H1",
  He2: "H2",
  He3: "H3",
  He4: "H4",
  He5: "H5",
};

const DIMENSION_LABELS = {
  career: "Nghề nghiệp",
  health: "Sức khỏe",
  learning: "Học tập",
  happiness: "Hạnh phúc",
  contribution: "Cống hiến",
};

const SECTION_SYSTEM_PROMPT = [
  "Bạn là chuyên gia tư vấn trưởng thành cho BridgeUp.",
  "Trả lời gọn nhưng đủ ý cốt lõi, bám đúng dữ liệu khảo sát 5 đỉnh.",
  "Không lan man, không lặp ý, không thêm thông tin ngoài dữ liệu được cung cấp.",
  "Khi phân tích từng đỉnh, tuyệt đối không trộn nội dung của đỉnh khác.",
].join("\n");

const CAREER_BIAS_PATTERN =
  /(nghề nghiệp|sự nghiệp|công việc|thăng tiến|chức vụ|vị trí|team lead|manager|director|ceo|founder)/i;
const MAX_DIMENSION_FIELD_WORDS = 30;
const STRENGTH_MIN_WORDS_BY_KEY = {
  learning: 5,
  contribution: 5,
};
const TOPIC_KEYWORDS_BY_KEY = {
  learning: ["học", "học tập", "tự học", "kỹ năng", "kiến thức", "chứng chỉ", "khóa", "trình độ", "learning"],
  contribution: ["cống hiến", "đóng góp", "cho đi", "hỗ trợ", "cộng đồng", "gia đình", "xã hội", "impact"],
};

const MODEL_TIMEOUT_MS = 16000;
const DEFAULT_MAX_TOKENS = 180;

let cachedQuestions = null;
let surveyQuestionTextSet = new Set();
let surveyQuestionTextList = [];
let surveyOptionLabelSet = new Set();

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

function sanitizeModelText(text = "") {
  return text
    .replace(/```(?:json)?\s*([\s\S]*?)```/gi, "$1")
    .replace(/`([^`]+)`/g, "$1")
    .replace(/^\s{0,3}#{1,6}\s*/gm, "")
    .replace(/^\s*>\s?/gm, "")
    .replace(/\*\*([^*]+)\*\*/g, "$1")
    .replace(/\*([^*]+)\*/g, "$1")
    .replace(/__([^_]+)__/g, "$1")
    .replace(/_([^_]+)_/g, "$1")
    .replace(/^\s*[-*+]\s+/gm, "")
    .replace(/\r/g, "")
    .trim();
}

function cleanField(value) {
  return sanitizeModelText(typeof value === "string" ? value : "").replace(/\n+/g, " ").trim();
}

function normalizeForCompare(value = "") {
  return sanitizeModelText(value)
    .toLowerCase()
    .replace(/[?!.:,;"'`()[\]{}]/g, "")
    .replace(/\s+/g, " ")
    .trim();
}

function stableIndex(seed = "", size = 1) {
  if (!size || size <= 1) return 0;
  let hash = 0;
  for (let i = 0; i < seed.length; i += 1) {
    hash = (hash * 31 + seed.charCodeAt(i)) >>> 0;
  }
  return hash % size;
}

function toSingleSentence(value = "") {
  const cleaned = cleanField(value).replace(/\s+/g, " ").trim();
  if (!cleaned) return "";

  return (
    cleaned
      .split(/(?<=[.!?])\s+|\n|;\s+/)
      .map((item) => item.trim())
      .find((item) => item.length > 0) || cleaned
  );
}

function toMaxTwoSentences(value = "") {
  const cleaned = cleanField(value).replace(/\s+/g, " ").trim();
  if (!cleaned) return "";

  const segments = cleaned
    .split(/(?<=[.!?])\s+|\n|;\s+/)
    .map((item) => item.trim())
    .filter(Boolean);

  if (!segments.length) return cleaned;
  return segments.slice(0, 2).join(" ");
}

function normalizeDimensionField(value) {
  const cleaned = cleanField(value)
    .replace(/[",}\]]+\s*$/g, "")
    .replace(/^["[{(\s]+/g, "")
    .replace(/^(?:json\s*(?:đáp án|output)?\s*:\s*)/i, "")
    .replace(/^(?:mô tả chi tiết về đỉnh[^:]*:\s*)/i, "")
    .replace(/^(?:1\s*câu\s*ngắn[^:]*:\s*)/i, "")
    .replace(/\b(?:json\s*đáp\s*án|json\s*sai\s*format|mô\s*tả\s*chi\s*tiết\s*về\s*đỉnh)\b[\s\S]*$/i, "")
    .replace(/\b(điểm tựa|nền tảng)\b/gi, "")
    .replace(/\b(ưu tiên cải thiện|hiện mới ở mức)\b/gi, "")
    .replace(/\s{2,}/g, " ")
    .trim();

  if (!cleaned) return "";
  if (/^\d+(?:\.\d+)?$/.test(cleaned)) return "";
  return cleaned;
}

function normalizeInsightField(value) {
  const cleaned = sanitizeModelText(typeof value === "string" ? value : "")
    .replace(/[",}\]]+\s*$/g, "")
    .replace(/^["[{(\s]+/g, "")
    .replace(/\r/g, "")
    .replace(/\n{2,}/g, "\n")
    .replace(/\b(điểm tựa|nền tảng)\b/gi, "")
    .replace(/\b(ưu tiên cải thiện|hiện mới ở mức)\b/gi, "")
    .replace(/\s{2,}/g, " ")
    .trim();

  if (!cleaned) return "";

  const noiLucMatch = cleaned.match(/nội\s*lực\s*:\s*([^\n]+)/i);
  const ngoaiLucMatch = cleaned.match(/ngoại\s*lực\s*:\s*([^\n]+)/i);

  if (noiLucMatch || ngoaiLucMatch) {
    const noiLuc = cleanField(noiLucMatch?.[1] || "");
    const ngoaiLuc = cleanField(ngoaiLucMatch?.[1] || "");
    const merged = [noiLuc, ngoaiLuc].filter(Boolean).join(" ");
    return toMaxTwoSentences(merged);
  }

  return toMaxTwoSentences(cleaned);
}

function containsCareerBias(value = "", key = "") {
  if (key === "career") return false;
  return CAREER_BIAS_PATTERN.test(sanitizeModelText(value));
}

function sanitizeDimensionByKey(value = "", key = "") {
  const normalized = normalizeDimensionField(value);
  if (!normalized) return "";
  if (containsCareerBias(normalized, key)) return "";
  return toSingleSentence(normalized);
}

function sanitizeInsightByKey(value = "", key = "") {
  const normalized = normalizeInsightField(value);
  if (!normalized) return "";
  if (containsCareerBias(normalized, key)) return "";
  return normalized;
}

function isSurveyEcho(value) {
  const normalized = sanitizeModelText(value).toLowerCase().trim();
  return surveyQuestionTextSet.has(normalized) || surveyOptionLabelSet.has(normalized);
}

function isSurveyLikeField(value = "") {
  const candidate = normalizeForCompare(value);
  if (!candidate) return false;

  if (surveyQuestionTextSet.has(candidate)) return true;

  return surveyQuestionTextList.some((questionText) => {
    const normalizedQuestion = normalizeForCompare(questionText);
    return (
      candidate === normalizedQuestion ||
      candidate.startsWith(normalizedQuestion) ||
      normalizedQuestion.startsWith(candidate)
    );
  });
}

function isValidDimensionField(value, field) {
  const normalized = normalizeDimensionField(value);
  const wordCount = (normalized.match(/\S+/g) || []).length;
  const sentenceCount = (normalized.match(/[.!?]+/g) || []).length;
  const leakedPromptPattern =
    /(chỉ được trả về json hợp lệ|json sai format sẽ bị reject|không thêm text ngoài json|không dùng markdown)/i;

  if (!normalized || isSurveyEcho(normalized)) return false;
  if (leakedPromptPattern.test(normalized)) return false;
  if ((field === "strength" || field === "weak") && wordCount < 3) return false;
  if ((field === "strength" || field === "weak") && (wordCount > 45 || sentenceCount > 2)) return false;

  return true;
}

function isValidDimensionFieldForKey(value, field, key) {
  const normalized = sanitizeDimensionByKey(value, key);
  if (!isValidDimensionField(normalized, field)) return false;
  if (!normalized) return false;
  if (containsCareerBias(normalized, key)) return false;
  if (normalized.includes("?") || isSurveyLikeField(normalized)) return false;

  const wordCount = (normalized.match(/\S+/g) || []).length;
  if (wordCount > MAX_DIMENSION_FIELD_WORDS) return false;

  const minWords = STRENGTH_MIN_WORDS_BY_KEY[key];
  if (field === "strength" && minWords && wordCount < minWords) return false;

  const topicKeywords = TOPIC_KEYWORDS_BY_KEY[key] || [];
  if (field === "strength" && topicKeywords.length > 0) {
    const lower = normalizeForCompare(normalized);
    const hasTopicSignal = topicKeywords.some((keyword) => lower.includes(normalizeForCompare(keyword)));
    if (!hasTopicSignal) return false;
  }

  return true;
}

function stripPolarityTerms(text = "") {
  return normalizeForCompare(text)
    .replace(/\b(nội lực|ngoại lực|mạnh|yếu|còn yếu|điểm mạnh|điểm yếu|hiện mới ở mức|cho thấy bạn đang có nền tảng|cho thấy bạn)\b/g, "")
    .replace(/\s+/g, " ")
    .trim();
}

function isLikelyDuplicateStrengthWeak(strength = "", weak = "") {
  const left = normalizeForCompare(strength);
  const right = normalizeForCompare(weak);

  if (!left || !right) return false;
  if (left === right || left.includes(right) || right.includes(left)) return true;

  const leftCore = stripPolarityTerms(left);
  const rightCore = stripPolarityTerms(right);
  if (leftCore && rightCore && (leftCore === rightCore || leftCore.includes(rightCore) || rightCore.includes(leftCore))) {
    return true;
  }

  const leftTokens = new Set(leftCore.split(" ").filter((token) => token.length > 2));
  const rightTokens = new Set(rightCore.split(" ").filter((token) => token.length > 2));
  if (!leftTokens.size || !rightTokens.size) return false;

  let intersection = 0;
  for (const token of leftTokens) {
    if (rightTokens.has(token)) intersection += 1;
  }

  const union = new Set([...leftTokens, ...rightTokens]).size;
  const jaccard = union > 0 ? intersection / union : 0;
  return jaccard >= 0.58;
}

function sumScores(scoreMap, ids) {
  return ids.reduce((total, id) => total + Number(scoreMap[id] || 0), 0);
}

function normalizeScore(value, maxScore) {
  if (!maxScore) return 0;
  return Math.round((Number(value || 0) / maxScore) * 100);
}

function calculateCategoryScores(rawScores = {}, scoringConfig = {}) {
  const careerSlopeCap =
    typeof scoringConfig.careerSlopeCap === "number"
      ? scoringConfig.careerSlopeCap
      : DEFAULT_CAREER_SLOPE_CAP;

  const careerRaw = Number(rawScores.C1 || 0) + sumScores(rawScores, ["C2", "C3"]) * careerSlopeCap;
  const learningRaw = sumScores(rawScores, ["L1", "L2", "L3", "L4", "L5"]);
  const healthRaw = sumScores(rawScores, ["H1", "H2", "H3", "H4", "H5"]);
  const happinessRaw = sumScores(rawScores, ["Ha1", "Ha2", "Ha3", "Ha4"]);
  const contributionRaw = sumScores(rawScores, ["Co1", "Co2", "Co3"]);

  return {
    career: Math.min(CATEGORY_MAX_SCORES.career, Number(careerRaw.toFixed(2))),
    health: normalizeScore(healthRaw, CATEGORY_MAX_SCORES.health),
    learning: normalizeScore(learningRaw, CATEGORY_MAX_SCORES.learning),
    happiness: Math.min(CATEGORY_MAX_SCORES.happiness, happinessRaw),
    contribution: Math.min(CATEGORY_MAX_SCORES.contribution, contributionRaw),
  };
}

function normalizeSurveyPayload(body = {}) {
  const normalizeQuestionId = (id) => LEGACY_QUESTION_ID_MAP[id] || id;
  const answers = Object.fromEntries(
    Object.entries(body.answers || {}).map(([id, value]) => [normalizeQuestionId(id), value])
  );

  let rawScores = Object.fromEntries(
    Object.entries(body.rawScores || {}).map(([id, value]) => [normalizeQuestionId(id), Number(value || 0)])
  );

  if (Object.keys(rawScores).length === 0 && Object.keys(answers).length > 0) {
    rawScores = Object.fromEntries(
      Object.entries(answers).map(([id, value]) => [id, Number(value?.score || 0)])
    );
  }

  const scores =
    body.scores && typeof body.scores === "object"
      ? calculateCategoryScores(rawScores, body.scoring || {})
      : calculateCategoryScores(rawScores, body.scoring || {});

  return { answers, rawScores, scores };
}

async function loadSurveyQuestions() {
  if (cachedQuestions) return cachedQuestions;
  const file = path.resolve(__dirname, "../../data/bridgeupSurveyQuestions.json");
  const raw = await fs.readFile(file, "utf8");
  cachedQuestions = JSON.parse(raw);

  surveyQuestionTextSet = new Set(
    cachedQuestions.map((question) => sanitizeModelText(question.question).toLowerCase())
  );
  surveyQuestionTextList = cachedQuestions.map((question) => sanitizeModelText(question.question).toLowerCase());
  surveyOptionLabelSet = new Set(
    cachedQuestions.flatMap((question) => (question.options || []).map((opt) => sanitizeModelText(opt.label).toLowerCase()))
  );

  return cachedQuestions;
}

function getQuestionMaxScore(question = {}) {
  if (Number(question.maxScore) > 0) return Number(question.maxScore);
  const optionScores = (question.options || []).map((option) => Number(option.score || 0));
  return optionScores.length ? Math.max(...optionScores) : 0;
}

function buildAnsweredQuestionDetails(questions, answers = {}, category) {
  return questions
    .filter((question) => !category || question.category === category)
    .filter((question) => answers[question.id]?.label)
    .map((question) => ({
      id: question.id,
      questionId: question.id,
      category: question.category,
      analysisType: question.analysisType || "",
      question: question.question,
      answer: answers[question.id].label,
      score: Number(answers[question.id].score || 0),
      maxScore: getQuestionMaxScore(question),
    }));
}

function buildDimensionQuestionGroups(questions, answers, category) {
  const answeredQuestions = buildAnsweredQuestionDetails(questions, answers, category);
  return {
    answeredQuestions,
    objective: answeredQuestions.filter((item) => item.analysisType === "objective"),
    subjective: answeredQuestions.filter((item) => item.analysisType === "subjective"),
  };
}

function buildAnswerSummaryLines(questions = []) {
  return questions.map((item) => `- ${item.question}: ${item.answer} (điểm: ${item.score})`).join("\n");
}

function buildProfileContext(questions, answers, scores) {
  const allAnswers = buildAnsweredQuestionDetails(questions, answers);
  return [
    "Chân dung người dùng từ câu trả lời:",
    buildAnswerSummaryLines(allAnswers),
    "",
    "Điểm các đỉnh:",
    JSON.stringify(scores, null, 2),
  ].join("\n");
}

function buildCurrentPortraitPrompt(questions, answers, scores) {
  return [
    buildProfileContext(questions, answers, scores),
    "",
    "Trả về thông tin cho phần \"Chân dung hiện tại\".",
    "Hãy đọc người dùng như một con người cụ thể từ các câu trả lời, không chỉ từ điểm số.",
    "Tập trung mô tả trạng thái nổi bật nhất của họ ở hiện tại: cách họ đang sống, đang vận hành, hoặc đang tự nhìn mình.",
    "Chỉ trả về 1-3 câu, gọn nhưng đủ ý cốt lõi.",
    "Không thêm bất kỳ dòng nào khác, xưng hô: \"BẠN\".",
  ].join("\n");
}

function buildNextActionPrompt(questions, answers, scores) {
  return [
    buildProfileContext(questions, answers, scores),
    "",
    "Trả về thông tin cho phần \"Hành động tiếp theo\".",
    "Hãy chọn đúng một hành động ngắn, cụ thể, sát nhất với người dùng này dựa trên điểm nghẽn quan trọng nhất.",
    "Phân tích phải có chiều sâu: chỉ rõ nội lực nào đang cản trở hoặc nâng đỡ, ngoại lực nào đang thiếu hoặc có thể tận dụng.",
    "Ưu tiên hành động có thể bắt đầu ngay trong 24-72 giờ tới.",
    "Chỉ trả về 1-3 câu, không checklist dài.",
    "Không thêm bất kỳ dòng nào khác, xưng hô: \"BẠN\".",
  ].join("\n");
}

function buildDimensionPromptTemplate({ key, score, objective, subjective, avoidHints = {} }) {
  const topicGuard = {
    career:
      "Chỉ tập trung chủ đề nghề nghiệp/sự nghiệp. Không phân tích lan sang sức khỏe, học tập, hạnh phúc, cống hiến nếu dữ liệu không nêu trực tiếp.",
    health:
      "Chỉ tập trung chủ đề sức khỏe thể chất/tinh thần, thói quen sinh hoạt. Không dùng góc nhìn nghề nghiệp/sự nghiệp/công việc.",
    learning:
      "Chỉ tập trung chủ đề học tập, kỹ năng, thói quen học. Không dùng góc nhìn nghề nghiệp/sự nghiệp/công việc.",
    happiness:
      "Chỉ tập trung chủ đề hạnh phúc, quan hệ, trạng thái nội tâm. Không dùng góc nhìn nghề nghiệp/sự nghiệp/công việc.",
    contribution:
      "Chỉ tập trung chủ đề cống hiến cho gia đình/xã hội. Không dùng góc nhìn nghề nghiệp/sự nghiệp/công việc.",
  };

  const questionBlock = [...objective, ...subjective].map((item) => ({
    question: item.question,
    answer: item.answer,
    score: item.score,
    analysisType: item.analysisType,
  }));

  const priorStrengthHints = Array.isArray(avoidHints?.strength) ? avoidHints.strength.filter(Boolean).slice(-4) : [];
  const priorWeakHints = Array.isArray(avoidHints?.weak) ? avoidHints.weak.filter(Boolean).slice(-4) : [];

  return [
    `Nhiệm vụ: phân tích riêng đỉnh ${DIMENSION_LABELS[key]}.`,
    "Chỉ dùng dữ liệu của đỉnh này, không suy diễn sang đỉnh khác.",
    topicGuard[key] || "",
    "",
    "Dữ liệu đầu vào:",
    JSON.stringify({ key, score, objective, subjective, questionBlock }, null, 2),
    "",
    "Trả về DUY NHẤT một JSON object hợp lệ với đúng 3 key:",
    '{"strength":"...","weak":"...","insight":"..."}',
    "",
    "Ràng buộc:",
    "1. Viết tiếng Việt tự nhiên, giàu ngữ nghĩa, tránh sáo rỗng.",
    "2. Không được lặp lại hoặc diễn đạt lại đơn thuần câu hỏi/đáp án survey; phải biến dữ liệu thành nhận định có suy luận.",
    "3. strength là 1 câu: nêu điểm mạnh cụ thể + tín hiệu hành vi lặp lại (thói quen/cách phản ứng) + tác động tích cực gần nhất.",
    "4. weak là 1 câu: nêu điểm nghẽn gốc (không chỉ triệu chứng) + hệ quả hiện tại + rủi ro nếu kéo dài.",
    "5. insight là 1-2 câu: chỉ ra mẫu vận hành cốt lõi của người dùng (điểm tựa vs. điểm nghẽn) và đòn bẩy thay đổi quan trọng nhất lúc này.",
    "6. Ưu tiên lập luận nhân quả rõ ràng (vì... nên...); có thể nêu mâu thuẫn ngầm giữa điều người dùng muốn và cách họ đang làm.",
    "7. Tránh từ chung chung như 'cố gắng hơn', 'cần cải thiện'; thay bằng nhận định cụ thể, có ngữ cảnh hành vi.",
    "8. Không dùng các mẫu câu khuôn cứng: 'Bạn mạnh ở...', 'Bạn còn yếu ở...', 'Bạn đang có điểm tựa...'.",
    "9. CẤM dùng các cụm từ sau trong mọi field: 'điểm tựa', 'nền tảng', 'ưu tiên cải thiện', 'hiện mới ở mức'.",
    "10. Mỗi field phải mở câu khác nhau và dùng động từ cụ thể (ví dụ: duy trì, chững lại, lệch nhịp, bỏ sót, bứt lên).",
    "11. Insight phải nêu 1 giả thuyết nguyên nhân gốc + 1 đòn bẩy hành động, tránh kết luận mô tả thuần túy.",
    "12. Không thêm text ngoài JSON.",
    "13. Tổng insight tối đa 39 từ.",
    "14. Strength và Weak của đỉnh này phải khác rõ ràng so với các gợi ý trước đó nếu có.",
    "",
    "Phong cách tham chiếu (không chép nguyên văn):",
    '- strength: "Nhịp học đều hằng tuần cho thấy bạn có năng lực tự quản tốt, nhờ đó tiến độ nâng kỹ năng không bị đứt đoạn."',
    '- weak: "Điểm nghẽn nằm ở việc ít vận động, nên năng lượng nền dễ hụt và khó duy trì hiệu suất ổn định trong ngày."',
    '- insight: "Bạn giữ được nhịp ở phần mình chủ động kiểm soát, nhưng dễ hụt ở phần cần phối hợp hoặc cam kết dài hơi. Đòn bẩy lúc này là chốt một tín hiệu nhắc cố định mỗi ngày để giảm trễ thực thi."',
    priorStrengthHints.length ? `Không lặp lại các ý strength sau: ${JSON.stringify(priorStrengthHints)}` : "",
    priorWeakHints.length ? `Không lặp lại các ý weak sau: ${JSON.stringify(priorWeakHints)}` : "",
  ].join("\n");
}

function buildDimensionPrompt(questions, answers, scores, key, avoidHints = {}) {
  const groups = buildDimensionQuestionGroups(questions, answers, key);
  return buildDimensionPromptTemplate({
    key,
    score: Number(scores[key] || 0),
    objective: groups.objective,
    subjective: groups.subjective,
    avoidHints,
  });
}

function parseDimensionResponse(text = "") {
  const cleaned = sanitizeModelText(text);
  const fallbackResult = {
    strength: "",
    weak: "",
    insight: "Cần thêm dữ liệu để đưa ra insight rõ ràng",
  };

  function parseLabeledFields(source) {
    const normalized = source.replace(/\r/g, "").trim();
    const patterns = {
      strength:
        /(?:^|\n|,)\s*"?\s*(?:strength|điểm mạnh(?: nổi bật nhất)?|diem manh(?: noi bat nhat)?)\s*"?\s*:\s*([\s\S]*?)(?=(?:\n|,)\s*"?\s*(?:weak|điểm yếu(?: lớn nhất)?|diem yeu(?: lon nhat)?|insight)\s*"?\s*:|$)/i,
      weak:
        /(?:^|\n|,)\s*"?\s*(?:weak|điểm yếu(?: lớn nhất)?|diem yeu(?: lon nhat)?)\s*"?\s*:\s*([\s\S]*?)(?=(?:\n|,)\s*"?\s*(?:strength|điểm mạnh(?: nổi bật nhất)?|diem manh(?: noi bat nhat)?|insight)\s*"?\s*:|$)/i,
      insight:
        /(?:^|\n|,)\s*"?\s*(?:insight)\s*"?\s*:\s*([\s\S]*?)(?=(?:\n|,)\s*"?\s*(?:strength|điểm mạnh(?: nổi bật nhất)?|diem manh(?: noi bat nhat)?|weak|điểm yếu(?: lớn nhất)?|diem yeu(?: lon nhat)?)\s*"?\s*:|$)/i,
    };

    const parsed = Object.fromEntries(
      Object.entries(patterns).map(([key, pattern]) => {
        const match = normalized.match(pattern);
        const rawValue = match?.[1] || "";
        const cleanedValue =
          key === "insight"
            ? sanitizeModelText(rawValue).replace(/^"+|"+$/g, "").trim()
            : cleanField(rawValue).replace(/^"+|"+$/g, "").trim();
        return [key, cleanedValue];
      })
    );

    if (parsed.strength || parsed.weak || parsed.insight) return parsed;
    return null;
  }

  try {
    const objectMatches = cleaned.match(/\{[\s\S]*?\}/g);
    let parsed = null;

    if (objectMatches?.length) {
      const parsedObjects = objectMatches
        .map((jsonChunk) => {
          try {
            return JSON.parse(jsonChunk);
          } catch {
            return null;
          }
        })
        .filter((chunk) => chunk && typeof chunk === "object");

      for (let index = parsedObjects.length - 1; index >= 0; index -= 1) {
        const chunk = parsedObjects[index];
        if ("strength" in chunk || "weak" in chunk || "insight" in chunk) {
          parsed = chunk;
          break;
        }
      }
      if (!parsed) parsed = parsedObjects[parsedObjects.length - 1] || null;
    }

    if (!parsed || Object.keys(parsed).length === 0) parsed = parseLabeledFields(cleaned);

    if (!parsed || Object.keys(parsed).length === 0) {
      const jsonMatch = cleaned.match(/\{[\s\S]*/);
      if (!jsonMatch) throw new Error("No JSON");

      let jsonString = jsonMatch[0].trim();
      if (!jsonString.endsWith("}")) jsonString = `${jsonString}}`;
      parsed = JSON.parse(jsonString);
    }

    return {
      strength: normalizeDimensionField(parsed.strength || "") || fallbackResult.strength,
      weak: normalizeDimensionField(parsed.weak || "") || fallbackResult.weak,
      insight: normalizeInsightField(parsed.insight || "") || fallbackResult.insight,
    };
  } catch {
    const fallback = parseLabeledFields(cleaned);
    if (fallback) {
      return {
        strength: normalizeDimensionField(fallback.strength || "") || fallbackResult.strength,
        weak: normalizeDimensionField(fallback.weak || "") || fallbackResult.weak,
        insight: normalizeInsightField(fallback.insight || "") || fallbackResult.insight,
      };
    }
    return fallbackResult;
  }
}

function buildFallbackDimensionField(questions, answers = {}, key, field) {
  const answeredQuestions = buildAnsweredQuestionDetails(questions, answers, key)
    .map((item) => ({
      ...item,
      normalizedScore: item.maxScore > 0 ? Number(item.score || 0) / item.maxScore : 0,
    }))
    .filter((item) => item.answer);

  if (!answeredQuestions.length) {
    return field === "strength" ? "Chưa xác định rõ điểm mạnh" : "Chưa xác định rõ điểm cần cải thiện";
  }

  const sortedQuestions = [...answeredQuestions].sort((left, right) => {
    if (field === "strength") return right.normalizedScore - left.normalizedScore;
    return left.normalizedScore - right.normalizedScore;
  });

  const selected = sortedQuestions[0];
  const questionText = cleanField(selected.question).replace(/[?!.]+$/g, "");
  const answerText = cleanField(selected.answer);
  if (!questionText || !answerText) {
    return field === "strength" ? "Chưa xác định rõ điểm mạnh" : "Chưa xác định rõ điểm cần cải thiện";
  }

  const q = questionText.toLowerCase();
  const a = answerText.toLowerCase();
  const variantSeed = `${key}:${field}:${q}:${a}`;

  if (field === "strength") {
    const options = [
      `Bạn đang giữ nhịp khá tốt ở ${q} (${a}), nên đây là phần có thể tận dụng để kéo tiến độ chung đi lên.`,
      `Ở mảng ${q}, bạn thể hiện độ ổn định rõ rệt (${a}); nếu duy trì nhịp này, nhiều mục tiêu khác sẽ dễ bứt hơn.`,
      `Điểm sáng nằm ở ${q} (${a}), cho thấy bạn đã có một thói quen hiệu quả để làm trụ đỡ cho giai đoạn tiếp theo.`,
    ];
    return options[stableIndex(variantSeed, options.length)];
  }

  const options = [
    `Điểm nghẽn hiện nằm ở ${q} (${a}), nếu để kéo dài sẽ làm nhịp phát triển dễ chững lại.`,
    `Phần ${q} (${a}) đang là chỗ hụt lực, nên hiệu quả tổng thể chưa bật lên như kỳ vọng.`,
    `Nút thắt dễ thấy là ${q} (${a}); nếu chưa xử lý sớm, bạn sẽ khó giữ được đà tiến ổn định.`,
  ];
  return options[stableIndex(variantSeed, options.length)];
}

function buildDistinctWeakFallback(questions, answers = {}, key = "", strengthText = "", forceAlternative = false) {
  const answeredQuestions = buildAnsweredQuestionDetails(questions, answers, key)
    .map((item) => ({
      ...item,
      normalizedScore: item.maxScore > 0 ? Number(item.score || 0) / item.maxScore : 0,
    }))
    .filter((item) => item.answer)
    .sort((left, right) => left.normalizedScore - right.normalizedScore);

  if (!answeredQuestions.length) return "Chưa xác định rõ điểm cần cải thiện";

  const normalizedStrength = normalizeForCompare(strengthText);
  const selected = forceAlternative
    ? answeredQuestions[Math.min(1, answeredQuestions.length - 1)] || answeredQuestions[0]
    : answeredQuestions.find((item) => {
        const q = normalizeForCompare(item.question);
        const a = normalizeForCompare(item.answer);
        return !normalizedStrength.includes(q) && !normalizedStrength.includes(a);
      }) || answeredQuestions[Math.min(1, answeredQuestions.length - 1)] || answeredQuestions[0];

  const questionText = cleanField(selected.question).replace(/[?!.]+$/g, "");
  const answerText = cleanField(selected.answer);
  if (!questionText || !answerText) return "Chưa xác định rõ điểm cần cải thiện";
  const q = questionText.toLowerCase();
  const a = answerText.toLowerCase();
  const options = [
    `Điểm nghẽn dễ thấy là ${q} (${a}), nên cần xử lý sớm để tránh ảnh hưởng dây chuyền.`,
    `Mảng ${q} (${a}) đang kéo chậm tiến độ chung; xử lý sớm sẽ giúp các phần khác vận hành nhẹ hơn.`,
    `Bạn đang vướng ở ${q} (${a}), và đây là điểm cần gỡ trước để không bị hụt nhịp dài hạn.`,
  ];
  return options[stableIndex(`${key}:weak-alt:${q}:${a}`, options.length)];
}

function buildFallbackInsight(questions, answers = {}, key = "") {
  const answeredQuestions = buildAnsweredQuestionDetails(questions, answers, key)
    .map((item) => ({
      ...item,
      normalizedScore: item.maxScore > 0 ? Number(item.score || 0) / item.maxScore : 0,
    }))
    .filter((item) => item.answer);

  if (!answeredQuestions.length) {
    return "Chưa đủ dữ liệu để đưa ra insight rõ ràng.";
  }

  const strongest = [...answeredQuestions].sort((a, b) => b.normalizedScore - a.normalizedScore)[0];
  const weakest = [...answeredQuestions].sort((a, b) => a.normalizedScore - b.normalizedScore)[0];

  const strongQuestion = cleanField(strongest.question).replace(/[?!.]+$/g, "").toLowerCase();
  const weakQuestion = cleanField(weakest.question).replace(/[?!.]+$/g, "").toLowerCase();
  const strongAnswer = cleanField(strongest.answer).toLowerCase();
  const weakAnswer = cleanField(weakest.answer).toLowerCase();

  const options = [
    `Bạn đang làm tốt ở ${strongQuestion} (${strongAnswer}), nhưng đà phát triển vẫn bị kéo chậm bởi ${weakQuestion} (${weakAnswer}); đòn bẩy lúc này là biến phần yếu thành một nhịp hành động cố định hằng tuần.`,
    `Dù ${strongQuestion} (${strongAnswer}) đang là lợi thế, ${weakQuestion} (${weakAnswer}) vẫn tạo lực cản đáng kể; nếu chốt một thói quen nhỏ lặp lại đều, quỹ đạo tăng trưởng sẽ rõ rệt hơn.`,
    `Bạn có ưu thế ở ${strongQuestion} (${strongAnswer}), nhưng nút thắt vẫn nằm tại ${weakQuestion} (${weakAnswer}); khi phần này được xử lý theo lịch cố định, hiệu quả toàn cục sẽ bật lên.`,
  ];
  return options[stableIndex(`${key}:insight:${strongQuestion}:${weakQuestion}`, options.length)];
}

function buildDimensionPayload(questions, scores, key, parsed = {}, answers = {}) {
  const parsedStrength = sanitizeDimensionByKey(parsed.strength, key);
  const parsedWeak = sanitizeDimensionByKey(parsed.weak, key);
  const parsedInsight = sanitizeInsightByKey(parsed.insight, key);

  let strength = isValidDimensionFieldForKey(parsedStrength, "strength", key)
    ? parsedStrength
    : buildFallbackDimensionField(questions, answers, key, "strength");
  let weak = isValidDimensionFieldForKey(parsedWeak, "weak", key)
    ? parsedWeak
    : buildFallbackDimensionField(questions, answers, key, "weak");

  if (isLikelyDuplicateStrengthWeak(strength, weak)) {
    weak = buildDistinctWeakFallback(questions, answers, key, strength);
  }
  if (isLikelyDuplicateStrengthWeak(strength, weak)) {
    weak = buildDistinctWeakFallback(questions, answers, key, strength, true);
  }

  return {
    score: Number(scores[key] || 0),
    strength,
    weak,
    insight: parsedInsight || buildFallbackInsight(questions, answers, key),
  };
}

function getModelTarget() {
  const hfEndpoint = process.env.HF_ENDPOINT;
  const hfModel = process.env.MODEL_ID;
  const hfToken = process.env.HF_TOKEN;

  if (hfEndpoint && hfModel && hfToken) {
    return {
      client: new OpenAI({ baseURL: hfEndpoint, apiKey: hfToken }),
      model: hfModel,
      provider: "hf-endpoint",
    };
  }

  if (process.env.OPENAI_API_KEY) {
    return {
      client: new OpenAI({ apiKey: process.env.OPENAI_API_KEY }),
      model: OPENAI_MODEL,
      provider: "openai",
    };
  }

  return null;
}

function withTimeout(promise, timeoutMs) {
  return Promise.race([
    promise,
    new Promise((_, reject) => {
      setTimeout(() => reject(new Error(`Model timeout after ${timeoutMs}ms`)), timeoutMs);
    }),
  ]);
}

async function requestModel({
  prompt,
  temperature = 0.2,
  onToken,
  maxTokens = DEFAULT_MAX_TOKENS,
  retryOnStreamFailure = true,
  useStream = true,
}) {
  const target = getModelTarget();
  if (!target) {
    return { ok: false, provider: "fallback", model: "none", text: "" };
  }

  const messages = [
    { role: "system", content: SECTION_SYSTEM_PROMPT },
    { role: "user", content: prompt },
  ];

  const baseOptions = {
    model: target.model,
    messages,
    temperature,
    top_p: 0.9,
    max_tokens: maxTokens,
  };

  if (!useStream) {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), MODEL_TIMEOUT_MS);
    try {
      const completion = await withTimeout(
        target.client.chat.completions.create({
          ...baseOptions,
          signal: controller.signal,
        }),
        MODEL_TIMEOUT_MS
      );
      const content = completion.choices?.[0]?.message?.content || "";
      if (typeof onToken === "function" && content) onToken(content);
      return {
        ok: true,
        provider: target.provider,
        model: target.model,
        text: content,
      };
    } finally {
      clearTimeout(timer);
    }
  }

  let content = "";

  try {
    const streamController = new AbortController();
    const streamTimer = setTimeout(() => streamController.abort(), MODEL_TIMEOUT_MS);

    try {
      const streamResult = await target.client.chat.completions.create({
        ...baseOptions,
        stream: true,
        signal: streamController.signal,
      });

      for await (const chunk of streamResult) {
        const token = chunk.choices?.[0]?.delta?.content;
        if (!token) continue;
        content += token;
        if (typeof onToken === "function") onToken(token);
      }
    } finally {
      clearTimeout(streamTimer);
    }
  } catch {
    if (!retryOnStreamFailure) {
      return {
        ok: true,
        provider: target.provider,
        model: target.model,
        text: "",
      };
    }

    const completionController = new AbortController();
    const completionTimer = setTimeout(() => completionController.abort(), MODEL_TIMEOUT_MS);

    try {
      const completion = await target.client.chat.completions.create({
        ...baseOptions,
        signal: completionController.signal,
      });

      content = completion.choices?.[0]?.message?.content || "";
      if (typeof onToken === "function" && content) onToken(content);
    } finally {
      clearTimeout(completionTimer);
    }
  }

  return {
    ok: true,
    provider: target.provider,
    model: target.model,
    text: content,
  };
}

function buildCurrentPortraitFallback(scores = {}) {
  const entries = Object.entries(scores || {}).sort((a, b) => Number(b[1] || 0) - Number(a[1] || 0));
  const top = entries[0]?.[0] || "career";
  const low = entries[entries.length - 1]?.[0] || "health";
  const label = {
    career: "nghề nghiệp",
    health: "sức khỏe",
    learning: "học tập",
    happiness: "hạnh phúc",
    contribution: "cống hiến",
  };

  return (
    "Bạn đang có điểm tựa tốt ở " +
    (label[top] || top) +
    ", nhưng nhịp phát triển chưa đồng đều giữa các đỉnh. Hiện tại, đỉnh " +
    (label[low] || low) +
    " cần ưu tiên củng cố để tạo đà trưởng thành bền vững."
  );
}

function buildNextActionFallback(scores = {}) {
  const label = {
    career: "nghề nghiệp",
    health: "sức khỏe",
    learning: "học tập",
    happiness: "hạnh phúc",
    contribution: "cống hiến",
  };
  const entries = Object.entries(scores || {}).sort((a, b) => Number(a[1] || 0) - Number(b[1] || 0));
  const lowest = entries[0]?.[0] || "health";
  const highest = [...entries].sort((a, b) => Number(b[1] || 0) - Number(a[1] || 0))[0]?.[0] || "career";
  const lowestLabel = label[lowest] || lowest;
  const highestLabel = label[highest] || highest;

  return (
    "Trong 72 giờ tới, hãy ưu tiên một việc nhỏ nhưng đo được cho đỉnh " +
    lowestLabel +
    ", vì đây đang là điểm kéo chậm nhịp phát triển chung của bạn. " +
    "Mỗi ngày dành 20 phút cố định cho việc này và chốt kết quả theo dạng đã làm/chưa làm, đồng thời mượn điểm mạnh ở " +
    highestLabel +
    " để giữ kỷ luật thực hiện đều."
  );
}

export async function getBridgeupSurveyConfig() {
  const questions = await loadSurveyQuestions();
  return { questions, scoring: { careerSlopeCap: DEFAULT_CAREER_SLOPE_CAP } };
}

export async function warmupBridgeupModel() {
  try {
    const result = await requestModel({ prompt: 'Chỉ trả lời đúng 1 từ: "OK".', temperature: 0, maxTokens: 8 });
    return { ok: true, warmed: Boolean(result.ok) };
  } catch (error) {
    return { ok: false, warmed: false, message: error?.message || "Warmup failed" };
  }
}

export async function generateBridgeupCurrentPortrait(body = {}) {
  const questions = await loadSurveyQuestions();
  const { answers, scores } = normalizeSurveyPayload(body);
  const prompt = buildCurrentPortraitPrompt(questions, answers, scores);

  try {
    const result = await requestModel({
      prompt,
      temperature: 0.2,
      maxTokens: 120,
      retryOnStreamFailure: false,
      useStream: false,
    });

    const data = sanitizeModelText(result.text) || buildCurrentPortraitFallback(scores);
    return { status: 200, body: { ok: true, section: "currentPortrait", data } };
  } catch (error) {
    return {
      status: 200,
      body: {
        ok: true,
        section: "currentPortrait",
        data: buildCurrentPortraitFallback(scores),
        warning: error?.message || "LLM error",
      },
    };
  }
}

export async function generateBridgeupNextAction(body = {}) {
  const questions = await loadSurveyQuestions();
  const { answers, scores } = normalizeSurveyPayload(body);
  const prompt = buildNextActionPrompt(questions, answers, scores);

  try {
    const result = await requestModel({
      prompt,
      temperature: 0.2,
      maxTokens: 120,
      retryOnStreamFailure: false,
      useStream: false,
    });

    const data = sanitizeModelText(result.text) || buildNextActionFallback(scores);
    return { status: 200, body: { ok: true, section: "nextAction", data } };
  } catch (error) {
    return {
      status: 200,
      body: {
        ok: true,
        section: "nextAction",
        data: buildNextActionFallback(scores),
        warning: error?.message || "LLM error",
      },
    };
  }
}

export async function generateBridgeupDimension(body = {}, key) {
  if (!DIMENSION_KEYS.includes(key)) {
    return { status: 400, body: { ok: false, section: key, message: "Invalid dimension key" } };
  }

  const questions = await loadSurveyQuestions();
  const { answers, scores } = normalizeSurveyPayload(body);
  const prompt = buildDimensionPrompt(questions, answers, scores, key, body?.avoidHints || {});

  try {
    const result = await requestModel({ prompt, temperature: 0.35, maxTokens: 150, retryOnStreamFailure: false, useStream: false });
    const parsed = parseDimensionResponse(result.text);
    const data = buildDimensionPayload(questions, scores, key, parsed, answers);

    return { status: 200, body: { ok: true, section: key, data } };
  } catch (error) {
    const fallback = buildDimensionPayload(questions, scores, key, {}, answers);
    return {
      status: 200,
      body: {
        ok: true,
        section: key,
        data: fallback,
        warning: error?.message || "LLM error",
      },
    };
  }
}

export { DIMENSION_KEYS };













