#!/bin/bash
# PostToolUse hook — Claude가 .ts/.tsx 파일을 Write/Edit한 "직후" 그 파일을 처리한다.
# ① 자동 포맷(oxfmt): 판단 0의 기계 작업이라 실패로 막지 않고 그냥 정렬 → 포맷은 게이트 실패가 될 일이 없다.
# ② 위반 검사(oxlint+smell): 있으면 additionalContext로 주입 → Claude가 verify까지 안 기다리고 그 자리에서 고친다.
# authoring-time 강제 = 4층(husky)·verify 전에 가장 빠른 피드백 루프. (검사는 advisory, 포맷만 파일을 수정)
#
# stdin: PostToolUse JSON. stdout: additionalContext JSON.
# 성능: 단일 파일만, oxlint는 node_modules 있을 때만(비-type-aware=빠름). 없으면 조용히 skip.

set -uo pipefail

input=$(cat 2>/dev/null || true)

file=$(printf '%s' "$input" | node "$(dirname "$0")/read-json-field.mjs" tool_input.file_path 2>/dev/null)
[ -z "$file" ] && exit 0
case "$file" in
  *.ts|*.tsx) ;;
  *) exit 0 ;;
esac

# 프로젝트 루트는 CLAUDE_PROJECT_DIR이 아니라 "편집한 파일"에서 거슬러 올라가 찾는다.
# CLAUDE_PROJECT_DIR에만 의존하면, 세션을 다른 폴더에서 띄우고 이 앱을 편집할 때
# (모노레포 하위·sibling 폴더·additional working dir) 경로 접두어가 안 맞아 rel이 절대경로로
# 남고 아래 case에서 즉시 빠져나가 매 저장 무음 no-op이 된다 (실사용 관찰).
abs="$file"
case "$abs" in
  /*) ;;
  *) abs="${CLAUDE_PROJECT_DIR:-$PWD}/$file" ;;
esac
root=$(dirname "$abs")
while [ "$root" != "/" ] && [ ! -f "$root/package.json" ]; do
  root=$(dirname "$root")
done
# package.json(=node_modules·oxlint 설정의 자리)을 못 찾으면 검사할 수 없다
[ -f "$root/package.json" ] || exit 0
cd "$root" 2>/dev/null || exit 0
rel="${abs#"$root"/}"
# 여전히 절대경로면 루트 밖 파일 — 이 프로젝트 룰로 검사하지 않는다
case "$rel" in /*) exit 0 ;; esac

# ── 자동 포맷 (저장 즉시, 판단 0의 순수 기계 작업) ──
# 포맷은 "고칠 수 있는 걸 실패로 막는" 게 아니라 그냥 정렬한다 → verify의 format:check가 실패가 될 일이 없다.
# 생성물/vendored(shadcn·orval)만 제외(upstream 대조 churn 방지). 테스트·스크립트도 정렬한다.
case "$rel" in
  *node_modules*|*/shared/ui/*|*/_generated/*|*use-mobile.ts) ;;
  *)
    OXFMT="./node_modules/.bin/oxfmt"
    { [ -x "$OXFMT" ] && [ -f "$rel" ] && "$OXFMT" "$rel" >/dev/null 2>&1; } || true
    ;;
esac

# 생성물/vendored/테스트/스크립트는 lint/smell 스킵 (우리가 소유·강제하는 코드만)
case "$rel" in
  *node_modules*|*/shared/ui/*|*/_generated/*|*use-mobile.ts|*.test.*|*.spec.*|scripts/*) exit 0 ;;
esac
[ -f "$rel" ] || exit 0

findings=""
add() { findings="${findings}
- $1"; }

# ── oxlint 단일 파일 (빠른 비-type-aware) — 바이너리 있을 때만 ──
OXLINT="./node_modules/.bin/oxlint"
if [ -x "$OXLINT" ]; then
  # --format=unix: 위반이 없으면 빈 출력, 있으면 "file:line:col: msg" 한 줄씩 (기본 graphical 포맷은 grep 불가)
  lint=$("$OXLINT" --format=unix "$rel" 2>/dev/null | head -12 || true)
  [ -n "$lint" ] && add "oxlint:
$lint"
fi

# ── 파일 단위 빠른 smell grep (code-smell.sh 부분집합) ──
# 수동 메모이제이션 (React Compiler와 모순)
if grep -qE '\buseCallback\b|\buseMemo\b|\bmemo\(' "$rel" 2>/dev/null; then
  add "수동 메모이제이션(useCallback/useMemo/memo) 발견 → React Compiler가 자동 메모이즈한다. 제거하라 (P-013.1)."
fi
# boolean useState 2개+
bcount=$(grep -cE 'useState.*\((false|true)\)' "$rel" 2>/dev/null || true)
if [ "${bcount:-0}" -ge 2 ]; then
  add "boolean useState ${bcount}개 → 상호배타면 union literal('none'|'a'|'b')로 합치기. 모달은 overlay-kit(P-038)."
fi
# forwardRef (React 19 불필요)
grep -q 'forwardRef' "$rel" 2>/dev/null && add "forwardRef → React 19에선 불필요. ref를 일반 prop으로 받기."
# 인라인 hex 색
grep -qE 'style=\{\{[^}]*#[0-9a-fA-F]{3,6}' "$rel" 2>/dev/null && add "인라인 hex 색 → Tailwind theme 토큰 사용(P-005, project-theme). 인라인 style 금지."
# any
grep -qE ':\s*any\b|<any>|as any\b' "$rel" 2>/dev/null && add "any 타입 → 구체 타입 또는 unknown+narrowing (P-015)."
# let/var (const만)
grep -qE '(^|[^a-zA-Z_.])(let|var)[[:space:]]+[a-zA-Z_$]' "$rel" 2>/dev/null && add "let/var 사용 → const만. 재할당·누적은 es-toolkit(map/reduce/groupBy 등)으로 불변하게 (P-006.1)."
# Suspense/useSuspenseQuery인데 같은 파일에 ErrorBoundary 없음 (경계 co-location, P-041)
# 컴포넌트를 쪼개며 쿼리를 자식으로 옮길 때 경계가 부모에만 남는 실수가 반복돼 승격 (실사용 관찰 2회+ 재발)
case "$rel" in
  *.tsx)
    if grep -q '<Suspense\|SuspenseQuery' "$rel" 2>/dev/null && ! grep -q 'ErrorBoundary\|errorElement' "$rel" 2>/dev/null \
      && ! grep -q 'code-smell-ok(boundary)' "$rel" 2>/dev/null; then
      add "Suspense/useSuspenseQuery인데 이 파일에 ErrorBoundary 없음 → 쿼리 쓰는 컴포넌트는 로딩(Suspense)+에러(ErrorBoundary) 경계를 같은 파일에서 데리고 다닌다 (P-041). 분리했다면 새 파일에도 경계를."
    fi
    # 공개(export) 컴포넌트가 helper보다 아래 (code-smell #23 미러 — verify까지 안 가고 저장 즉시.
    # helper를 위에서부터 쌓다 보면 export가 파일 끝으로 밀리는 실수가 실사용에서 재발)
    if ! grep -q 'code-smell-ok(export-order)' "$rel" 2>/dev/null; then
      exp_ln=$(grep -nE '^export const [A-Za-z][A-Za-z0-9]* = \(|^export function [A-Za-z][A-Za-z0-9]*\(' "$rel" 2>/dev/null | head -1 | cut -d: -f1)
      if [ -n "$exp_ln" ]; then
        helper_ln=$(grep -nE '^const [A-Z][A-Za-z0-9]* = \(|^function [A-Z][A-Za-z0-9]*\(' "$rel" 2>/dev/null \
          | awk -F: -v e="$exp_ln" '$1 < e {print $1; exit}')
        [ -n "$helper_ln" ] && add "라인 ${helper_ln}의 helper 컴포넌트가 공개(export) 컴포넌트(라인 ${exp_ln})보다 위에 있음 → 공개 컴포넌트를 상수·타입 아래 최상단으로, helper는 그 아래로 (code-smell #23). 예외는 code-smell-ok(export-order): 사유."
      fi
    fi
    # react-table 인스턴스를 받는 컴포넌트에 'use no memo' (code-smell #24 미러 — React Compiler stale 렌더)
    if grep -q "@tanstack/react-table" "$rel" 2>/dev/null \
      && grep -qE 'type Table\b|TableInstance' "$rel" 2>/dev/null \
      && ! grep -q 'useReactTable' "$rel" 2>/dev/null \
      && ! grep -q 'use no memo' "$rel" 2>/dev/null \
      && ! grep -q 'code-smell-ok(table-memo)' "$rel" 2>/dev/null; then
      add "react-table의 table 인스턴스를 prop으로 받는 컴포넌트인데 use no memo 없음 → table은 같은 참조로 내부만 변이해 React Compiler 메모 시 데이터가 바뀌어도 표가 안 갱신된다(stale 렌더). 컴포넌트 본문 첫 줄에 'use no memo'; 추가 (code-smell #24)."
    fi
    ;;
esac

[ -z "$findings" ] && exit 0

msg="방금 편집한 \`$rel\` 에서 규칙 위반 신호가 감지됐습니다. **다음 작업으로 넘어가기 전에** 아래를 고치세요:${findings}

(이건 authoring-time 자동 검사입니다. 전체 검증은 \`pnpm verify\`.)"

printf '%s' "$msg" | node "$(dirname "$0")/emit-context.mjs"
exit 0
