/* ============================================================================
 * 서명전에 — iOS Document Preview (fullscreen reader)
 * ========================================================================== */

function PreviewScreen({ scenario = "jeonse", analysis = null, onBack, onOpenIssue, focusIssueId }) {
  const isReal = !!analysis;
  const scenarioData = window.SCENARIOS[scenario] || window.SCENARIOS.jeonse;

  /* 실제 분석 시: extracted_text + evidence_quote → 문단 + 하이라이트로 재구성 */
  const realData = useMemo(() => {
    if (!isReal) return null;
    /* 개인정보 (PII) 섹션 truncation —
       계약서 끝의 "임대인 주소 / 주민등록번호 / 전화 / 성명 / 중개업자
       사무소" 같은 서명란은 분석 결과와 무관할 뿐 아니라 화면에 그대로
       노출되면 개인정보 보호 측면에서 위험. PII 트리거가 나타나면 그
       직전까지만 잘라낸다.

       트리거: 주민등록번호 (가장 확실한 PII 키워드 — 본문 절대 등장 X) +
       라벨 + 콜론 패턴 ("임대인 주 소 :", "사무소 소재지 :" 등).
       newline 구분 없이 text 어디든 등장하면 trigger — docx 파싱이 한 줄로
       묶어 출력하는 케이스 (single line 으로 보관한다 → 임대인 주 소 → ...
       이 다 이어진 경우) 도 잡기 위함. 라벨 + 콜론 강제로 본문 prose 의
       단순 단어 언급은 제외. */
    const _PII_TRIGGER_RE = /(?:주민등록번호|임대인\s*주\s*소|임차인\s*주\s*소|개업공인중개사\s*확인|사무소\s*소재지|소속공인중개사|중개업자\s*사무소)\s*[:：]/;
    let text = (analysis.extracted_text || analysis.text_preview || "").trim();
    const piiMatch = text.match(_PII_TRIGGER_RE);
    if (piiMatch && typeof piiMatch.index === "number" && piiMatch.index > 0) {
      text = text.slice(0, piiMatch.index).trim();
    }
    const issues = (analysis.issues || []).map((iss, i) => ({ id: `srv-i${i}`, ...iss }));
    /* 문단 분할 — docx 파서 (parsers.py `_extract_docx`) 가 paragraph 단위로
       단일 `\n` 으로 join 하기 때문에, double `\n\n` section split 만으로는
       모든 줄이 한 덩어리로 묶임. 한국 계약서의 자연 구조 (제N조 / 금액
       라벨 / 부동산 표시 라벨) 에 맞춰 추가 split splitter 들을 묶어서
       파이프라인:
         (1) double \n\n 으로 section
         (2) section 내부에서 split 트리거 (각 라인 시작이 다음 중 하나):
             - "1. " "2. " 등 번호 항목
             - "제 N 조" 조항 마커
             - "보증금:" "계약금:" "중도금:" "잔금:" "차임:" "월세:" "관리비:" "임대차 기간:" 같은 금액·기간 라벨
             - "소재지:" "토지:" "건물:" "임대할부분:" "임대부분:" 같은 부동산 표시 라벨
             - "특약사항" 섹션 헤더
         (3) 최종 single \n 은 단락 내 줄바꿈 → 공백
       회귀: "월세 계약서" 같은 prose 의 "월세" 가 split 트리거되지 않게
       label 패턴은 반드시 line-start (\n 이후) + 라벨 + 콜론 형태로 한정. */
    const _SECTION_SPLIT_RE = new RegExp(
      "\\n(?=" +
        // 번호 항목
        "\\s*\\d+\\.\\s" +
        // 제 N 조
        "|\\s*제\\s*\\d+\\s*조" +
        // 금액 / 기간 라벨 + 콜론 (공백 허용)
        "|\\s*(?:보\\s*증\\s*금|계\\s*약\\s*금|중\\s*도\\s*금|잔\\s*금|차\\s*임|월\\s*세|월\\s*임대료|임대료|관\\s*리\\s*비|임대차\\s*기간|계약\\s*기간|존속\\s*기간)\\s*[:：]" +
        // 부동산 표시 라벨 + 콜론
        "|\\s*(?:소\\s*재\\s*지|토\\s*지|건\\s*물|임대할부분|임대부분|임대\\s*부분|면적|지목)\\s*[:：]" +
        // 특약사항 헤더 (line-start 면 다음에 \n 이나 EOL 이 와도 split 트리거)
        "|\\s*특약사항(?=\\s|$)" +
      ")",
      "g"
    );
    const rawParagraphs = text
      ? text
          // (1) 빈 줄 기준 section split
          .split(/\n\s*\n+/)
          // (2) section 내부에서 다양한 split trigger 적용
          .flatMap((section) => section.split(_SECTION_SPLIT_RE))
          // (3) 단일 \n 잔여는 단락 내 줄바꿈 → 공백
          .map((p, idx) => ({ id: `p${idx}`, text: p.trim().replace(/\s*\n\s*/g, " ") }))
          .filter((p) => p.text)
      : [];
    /* ── 매칭 전략 ───────────────────────────────────────────────────────
       이전 버전은 evidence_quote 의 [[K]] 마커를 단순 제거하고 "clean 텍스트
       전체" 를 `indexOf` 로 본문에서 찾았음. 문제: LLM 이 마커 바깥 문맥에
       본문과 한 끗 다른 공백/구두점 (예: "임차인은, " vs "임차인은") 을
       섞어 보내면 전체가 통째로 불일치로 떨어져 하이라이트 자체가 사라짐.
       특약 여러 개(3·4·5·6·7번) 가 다 위험인데 6번 하나만 마커 내외가 모두
       본문과 일치해서 유일하게 살아남는 회귀가 그 원인.

       새 전략:
         ① [[K]]...[[/K]] 마커 안 구절을 각각 꺼내 그걸 본문에서 찾는다.
            마커는 백엔드 `ensure_markers_in()` 이 모든 evidence_quote 에
            보장하므로 사실상 모든 이슈가 이 경로로 하이라이트된다.
         ② 마커 구절이 본문에 없거나 마커가 아예 없으면 evidence_quote 전체
            (마커 제거) 매칭으로 폴백.
         ③ 공백이 달라 ①·② 가 실패하면 공백만 지운 normalized 형태로 재탐색,
            매칭된 실제 substring 을 본문에서 복원해 돌려준다.
       ──────────────────────────────────────────────────────────────── */
    const _stripMarkers = (s) => (s || "").replace(/\[\[K\]\]([^\[]*?)\[\[\/K\]\]/g, "$1");
    const _extractMarked = (s) => {
      const out = [];
      const re = /\[\[K\]\]([^\[]*?)\[\[\/K\]\]/g;
      let m;
      while ((m = re.exec(s || ""))) {
        if (m[1] && m[1].trim()) out.push(m[1]);
      }
      return out;
    };
    /* 공백 무시 matching — haystack 안에서 needle 을 공백 차이까지 허용해
       찾고, 매칭된 원본 substring 을 돌려준다. 없으면 null.
       탐색은 해당 paragraph 단위라 크기가 크지 않아 O(n·slack) 로 충분. */
    const _findLoose = (haystack, needle) => {
      if (!needle) return null;
      if (haystack.indexOf(needle) >= 0) return needle;
      const ns = (s) => s.replace(/\s+/g, "");
      const needleN = ns(needle);
      if (!needleN) return null;
      if (ns(haystack).indexOf(needleN) < 0) return null;
      const maxSlack = Math.max(4, Math.floor(needle.length * 0.25));
      for (let start = 0; start < haystack.length; start++) {
        const maxEnd = Math.min(haystack.length, start + needle.length + maxSlack);
        for (let end = start + needleN.length; end <= maxEnd; end++) {
          if (ns(haystack.slice(start, end)) === needleN) {
            return haystack.slice(start, end);
          }
        }
      }
      return null;
    };
    const paraMarks = rawParagraphs.map((p) => {
      const marks = [];
      const seen = new Set(); // 같은 이슈의 같은 구절이 중복 추가되지 않도록
      const pushMark = (iss, found) => {
        const key = `${iss.id}::${found}`;
        if (seen.has(key)) return;
        seen.add(key);
        marks.push({
          from: found,
          issue_id: iss.id,
          severity: iss.severity,
          issue: iss,
        });
      };
      issues.forEach((iss) => {
        const eq = iss.evidence_quote || "";
        if (!eq) return;
        // ① 마커 안 구절 우선
        const markedPhrases = _extractMarked(eq);
        let hitByMarker = false;
        markedPhrases.forEach((phrase) => {
          const found = _findLoose(p.text, phrase);
          if (found) {
            pushMark(iss, found);
            hitByMarker = true;
          }
        });
        if (hitByMarker) return;
        // ② 마커 안 구절을 못 찾았으면 evidence_quote 전체(마커 제거) 로 폴백
        const clean = _stripMarkers(eq);
        if (!clean) return;
        const foundClean = _findLoose(p.text, clean);
        if (foundClean) pushMark(iss, foundClean);
      });
      return { ...p, marks };
    });
    const paragraphs = paraMarks;
    return {
      filename: analysis.filename || "올린 계약서",
      paragraphs: paraMarks,
      issues,
    };
  }, [isReal, analysis]);

  const data = isReal ? realData : scenarioData;
  const [scrolled, setScrolled] = useState(false);
  const scrollRef = useRef(null);

  const issuesCount = (data?.issues || []).filter(i => i.severity !== "low").length;

  /* Scroll to focus issue when provided */
  useEffect(() => {
    if (!focusIssueId || !scrollRef.current) return;
    const el = scrollRef.current.querySelector(`mark[data-id="${focusIssueId}"]`);
    if (el) {
      const container = scrollRef.current;
      const top = el.offsetTop - 80;
      container.scrollTo({ top, behavior: "smooth" });
    }
  }, [focusIssueId, isReal]);

  /* Render line with marks interpolated (mock scenarios OR real evidence_quote) */
  const renderLine = (line) => {
    if (!line.marks || line.marks.length === 0) return line.text;
    let text = line.text;
    const parts = [];
    let cursor = 0;
    const marks = [...line.marks]
      .map((m) => ({ ...m, idx: text.indexOf(m.from) }))
      .filter((m) => m.idx >= 0)
      .sort((a, b) => a.idx - b.idx);

    marks.forEach((m, i) => {
      if (m.idx > cursor) parts.push(text.slice(cursor, m.idx));
      const issue = m.issue || (data.issues || []).find((iss) => iss.id === m.issue_id);
      parts.push(
        <mark
          key={i}
          data-id={m.issue_id}
          className={m.severity === "medium" ? "m-warn" : ""}
          onClick={() => issue && onOpenIssue(issue)}
          style={{ cursor: "pointer" }}
        >
          {m.from}
        </mark>,
      );
      cursor = m.idx + m.from.length;
    });
    if (cursor < text.length) parts.push(text.slice(cursor));
    return parts;
  };

  return (
    <div className="app">
      <div className={"navbar" + (scrolled ? " navbar--scrolled" : "")}>
        <button className="navbar__btn" onClick={onBack} style={{ marginLeft: 2 }}>
          <IOSIcon name="chev-left" size={22} weight={2.4} />
          <span>결과</span>
        </button>
        <div className="navbar__title">{data.filename}</div>
        <button className="navbar__btn" style={{ marginRight: 6 }}>
          <IOSIcon name="share" size={21} />
        </button>
      </div>

      <div
        className="scroll scroll--plain"
        ref={scrollRef}
        onScroll={(e) => setScrolled(e.currentTarget.scrollTop > 8)}
      >
        <div className="doc-reader">
          {isReal ? (
            <>
              <h2 style={{ marginTop: 0 }}>{data.filename}</h2>
              {(data.paragraphs || []).length === 0 ? (
                <p style={{ color: "var(--label-secondary)", opacity: 0.7 }}>
                  읽어낸 글자가 없어요. 사진 화질이 낮거나 OCR이 제대로 되지 않았을 수 있으니 밝은 곳에서 다시 찍어 주세요.
                </p>
              ) : (
                (data.paragraphs || []).map((p) => (
                  <p key={p.id}>{renderLine(p)}</p>
                ))
              )}
            </>
          ) : (
            (data.source_lines || []).map((line, i) => {
              if (i === 0) {
                return <h2 key={line.id} style={{ marginTop: 0 }}>{line.text}</h2>;
              }
              return <p key={line.id}>{renderLine(line)}</p>;
            })
          )}

          <div style={{
            marginTop: 32,
            padding: "14px 16px",
            background: "var(--fill-3)",
            borderRadius: 12,
            fontSize: 13,
            color: "var(--label-secondary)",
            lineHeight: 1.5,
          }}>
            {isReal
              ? "올려 주신 계약서에서 읽어낸 글이에요. 색칠된 문구를 누르면 자세한 설명으로 넘어가요."
              : "원본 계약서에서 중요한 조항만 뽑아 보여드려요. 색칠된 문구를 누르면 자세한 설명으로 넘어가요."}
          </div>
        </div>

        {/* Sticky floating pill — 결과 화면(=ResultScreen) 으로 돌아가서
            위험 조항 목록을 보게 한다. 이전엔 onOpenIssue(data.issues[0]) 로
            첫 이슈만 띄워서 (a) 사용자 의도와 다르고 (b) issues 비면 sheet 가
            깨지는 회귀가 있었음. onBack 호출하면 stack 에서 preview 가 빠지고
            자연스럽게 ResultScreen 노출. */}
        {(data.issues || []).length > 0 && (
          <div style={{ padding: "0 0 16px" }}>
            <button className="reader-pill" onClick={onBack}>
              <span style={{ display: "inline-flex", alignItems: "center", gap: 8 }}>
                <IOSIcon name="shield-exclam" size={18} />
                위험 조항 보러 가기
              </span>
              <span className="reader-pill__count tnum">{issuesCount}</span>
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

window.PreviewScreen = PreviewScreen;

Object.assign(window, { PreviewScreen });
