// ═══════════════════════════════════════════════════════════════
// location-engine.jsx — 모듈⑥-A: 입지 분석 계산 엔진 (순수 JS)
//
// 판정 기준 출처: 시너지랩스 「부동산 분석 자동화 스킬 27종 활용가이드」(2026.07)
//   — 입지톡(ipzitalk) 역세권/병세권/쇼세권/숲세권/초품아 진단 +
//     교통/교육/생활 환경 분석의 공개 판정 기준을 그대로 상수화.
//
// ⚠ 방법론 주의사항 (가이드 원문 준용 — UI 면책에도 그대로 노출):
//   1. 모든 거리는 "직선거리" 기준 — 실제 도보 경로·시간과 다를 수 있음
//   2. 개통 예정 노선·개원 예정 시설은 반영하지 않음
//   3. 학교 배정(학구도)은 별도 확인 필요 — 거리≠배정
//   4. 폐업·휴업·이전 시설은 등급 기준에서 제외
//   5. 자료가 없으면 값을 추정하지 않고 "판정 유보"로 표시
//
// 데이터 계층:
//   - LOC_FACTS: 주요 단지의 대표 시설·직선거리 (공개 지도 기준 근사 실측)
//   - estimateLoc(): 나머지 단지는 id 시드해시 기반 결정적 추정 (estimated=true 표기)
// ═══════════════════════════════════════════════════════════════

// ─── 판정 기준 테이블 (가이드 결과화면 범례 그대로) ──────────────
const LOCATION_RULES = {
  asOf: '2026-07',
  source: '시너지랩스 부동산 분석 자동화 스킬 27종 활용가이드 (입지톡, 2026.07)',
  // 각 cuts: [상한(m, 이하), 등급 라벨] — 순서대로 tier 0,1,2,3
  subway: {
    radius: 2000,      // 역 조회 반경 2km
    lineRadius: 1000,  // 노선 수 집계 반경 1km (더블·트리플 표기 기준, 등급엔 미반영)
    cuts: [[500, '초역세권'], [800, '준역세권'], [1200, '조금 먼 편'], [Infinity, '비역세권']],
  },
  hospital: {
    radius: 3000,      // 종합·대학·국립병원급만 등급 기준 (일반 병원 제외)
    cuts: [[1000, '가까움'], [2000, '보통'], [3000, '조금 먼 편'], [Infinity, '먼 편']],
  },
  retail: {
    radius: 3000,      // 백화점·대형마트·대형슈퍼(SSM) — 입점매장 노이즈 제외
    cuts: [[1000, '가까움'], [2000, '보통'], [3000, '조금 먼 편'], [Infinity, '먼 편']],
  },
  park: {
    radius: 1500,      // 실제 공원만 (공원 시설물·상호명 노이즈 제외)
    cuts: [[300, '공원 바로 앞'], [700, '가까움'], [1200, '조금 먼 편'], [Infinity, '보통·먼 편']],
  },
  school: {
    // 초품아 진단 — 운영 중 초등학교 최근접 직선거리
    cuts: [[300, '초품아'], [500, '가까움'], [1000, '조금 먼 편'], [Infinity, '보통·먼 편']],
  },
  academyRadius: 700,    // 학원 밀집도 집계 반경 (교육 환경 분석)
  universityRadius: 3000, // 대학교 조회 반경 (교육 환경 분석)
  // tier(0~3) → 환경 점수 환산 기본값
  tierScore: [95, 78, 58, 38],
};

// ─── 주요 단지 실측 시설 데이터 (공개 지도 직선거리 근사) ─────────
// 형식: { subway:{name,line,dist,lines1km}, hospital, retail, park, school, academyCount700, publicFac }
const LOC_FACTS = {
  'eunma': { // 대치 은마 — 단지 내 대곡초(초품아로 유명), 대치학원가 핵심
    subway: { name: '대치역', line: '3호선', dist: 350, lines1km: 2 },
    hospital: { name: '삼성서울병원', dist: 1600 },
    retail: { name: '롯데백화점 강남점', dist: 1300 },
    park: { name: '양재천 산책로', dist: 450 },
    school: { name: '대곡초등학교', dist: 210 },
    academyCount700: 140, publicFac: { name: '대치1동주민센터', dist: 380 },
  },
  'raemian-daechi': { // 래미안대치팰리스 — 대치역 초인접
    subway: { name: '대치역', line: '3호선', dist: 280, lines1km: 2 },
    hospital: { name: '삼성서울병원', dist: 1400 },
    retail: { name: '롯데백화점 강남점', dist: 1100 },
    park: { name: '양재천 산책로', dist: 500 },
    school: { name: '대치초등학교', dist: 340 },
    academyCount700: 150, publicFac: { name: '대치4동주민센터', dist: 300 },
  },
  'reburi-park': { // 아크로리버파크 — 신반포역·한강공원
    subway: { name: '신반포역', line: '9호선', dist: 230, lines1km: 2 },
    hospital: { name: '가톨릭대 서울성모병원', dist: 1300 },
    retail: { name: '신세계백화점 강남점', dist: 1200 },
    park: { name: '반포한강공원', dist: 350 },
    school: { name: '잠원초등학교', dist: 420 },
    academyCount700: 45, publicFac: { name: '반포2동주민센터', dist: 500 },
  },
  'banpo-jay': { // 반포자이
    subway: { name: '고속터미널역', line: '9호선·3호선·7호선', dist: 600, lines1km: 3 },
    hospital: { name: '가톨릭대 서울성모병원', dist: 800 },
    retail: { name: '신세계백화점 강남점', dist: 750 },
    park: { name: '서리풀공원', dist: 550 },
    school: { name: '원촌초등학교', dist: 280 },
    academyCount700: 55, publicFac: { name: '반포본동주민센터', dist: 600 },
  },
  'jamsil-elsa': { // 잠실 엘스
    subway: { name: '잠실새내역', line: '2호선', dist: 260, lines1km: 2 },
    hospital: { name: '서울아산병원', dist: 2100 },
    retail: { name: '롯데백화점 잠실점', dist: 1400 },
    park: { name: '잠실한강공원', dist: 600 },
    school: { name: '잠실초등학교', dist: 250 },
    academyCount700: 60, publicFac: { name: '잠실2동주민센터', dist: 450 },
  },
  'heliocity': { // 가락 헬리오시티 — 송파역 초인접
    subway: { name: '송파역', line: '8호선', dist: 180, lines1km: 2 },
    hospital: { name: '경찰병원', dist: 900 },
    retail: { name: '가든파이브 현대시티몰', dist: 1600 },
    park: { name: '문정근린공원', dist: 700 },
    school: { name: '가락초등학교', dist: 240 },
    academyCount700: 50, publicFac: { name: '가락1동주민센터', dist: 400 },
  },
  'olympic-pkc': { // 올림픽파크포레온 (둔촌주공 재건축)
    subway: { name: '둔촌동역', line: '5호선·9호선', dist: 400, lines1km: 2 },
    hospital: { name: '한솔병원(종합)', dist: 1200 },
    retail: { name: '현대백화점 천호점', dist: 2300 },
    park: { name: '올림픽공원', dist: 500 },
    school: { name: '위례초등학교(단지 내)', dist: 200 },
    academyCount700: 35, publicFac: { name: '둔촌1동주민센터', dist: 550 },
  },
  'ap-hyundai': { // 압구정 현대
    subway: { name: '압구정역', line: '3호선', dist: 500, lines1km: 1 },
    hospital: { name: '강남세브란스병원', dist: 2800 },
    retail: { name: '갤러리아백화점 명품관', dist: 900 },
    park: { name: '압구정한강공원', dist: 300 },
    school: { name: '압구정초등학교', dist: 300 },
    academyCount700: 30, publicFac: { name: '압구정동주민센터', dist: 450 },
  },
  'trimage': { // 트리마제 — 서울숲 초인접
    subway: { name: '뚝섬역', line: '2호선·수인분당선', dist: 350, lines1km: 2 },
    hospital: { name: '한양대학교병원', dist: 1900 },
    retail: { name: '이마트 성수점', dist: 800 },
    park: { name: '서울숲', dist: 250 },
    school: { name: '경동초등학교', dist: 600 },
    academyCount700: 18, publicFac: { name: '성수1가1동주민센터', dist: 500 },
  },
  'hannam-dhill': { // 한남 더힐
    subway: { name: '한강진역', line: '6호선', dist: 650, lines1km: 1 },
    hospital: { name: '순천향대학교 서울병원', dist: 1400 },
    retail: { name: '현대백화점 압구정본점(한남대교 건너)', dist: 2400 },
    park: { name: '한남공원', dist: 400 },
    school: { name: '한남초등학교', dist: 550 },
    academyCount700: 10, publicFac: { name: '한남동주민센터', dist: 700 },
  },
  'pangyo-prujio': { // 판교 푸르지오월드마크
    subway: { name: '판교역', line: '신분당선·경강선', dist: 450, lines1km: 2 },
    hospital: { name: '분당서울대학교병원', dist: 2600 },
    retail: { name: '현대백화점 판교점', dist: 500 },
    park: { name: '화랑공원', dist: 350 },
    school: { name: '보평초등학교', dist: 480 },
    academyCount700: 25, publicFac: { name: '백현동주민센터', dist: 400 },
  },
  'gwanggyo-ho': { // 광교호반베르디움
    subway: { name: '광교중앙역', line: '신분당선', dist: 400, lines1km: 1 },
    hospital: { name: '아주대학교병원', dist: 2900 },
    retail: { name: '갤러리아백화점 광교점', dist: 800 },
    park: { name: '광교호수공원', dist: 450 },
    school: { name: '신풍초등학교', dist: 350 },
    academyCount700: 28, publicFac: { name: '광교1동주민센터', dist: 500 },
  },
  'songdo-1park': { // 송도 더샵 퍼스트파크
    subway: { name: '테크노파크역', line: '인천1호선', dist: 500, lines1km: 1 },
    hospital: { name: '인하대학교병원(송도 분원권)', dist: 2800 },
    retail: { name: '트리플스트리트·현대프리미엄아울렛', dist: 900 },
    park: { name: '송도센트럴파크', dist: 650 },
    school: { name: '송도초등학교', dist: 380 },
    academyCount700: 22, publicFac: { name: '송도2동주민센터', dist: 550 },
  },
  'lct-thesharp': { // 엘시티 더샵 — 해운대해수욕장 앞
    subway: { name: '중동역', line: '2호선', dist: 700, lines1km: 1 },
    hospital: { name: '인제대학교 해운대백병원', dist: 2200 },
    retail: { name: '신세계백화점 센텀시티(2정거장)', dist: 2900 },
    park: { name: '해운대해수욕장·동백공원', dist: 200 },
    school: { name: '해운대초등학교', dist: 500 },
    academyCount700: 15, publicFac: { name: '중1동주민센터', dist: 450 },
  },
  'beomeo-cp': { // 범어 센트럴푸르지오 — 수성학원가
    subway: { name: '범어역', line: '2호선', dist: 300, lines1km: 1 },
    hospital: { name: '경북대학교병원', dist: 2700 },
    retail: { name: '신세계백화점 대구점', dist: 2500 },
    park: { name: '범어공원', dist: 500 },
    school: { name: '범어초등학교', dist: 320 },
    academyCount700: 90, publicFac: { name: '범어1동주민센터', dist: 350 },
  },
};

// ─── 시드해시 (data.jsx buildApt와 동일 방식 — 결정적) ──────────
function locHash(id) {
  let h = 0;
  for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) % 100000;
  return h;
}

// ─── 태그에서 노선명 추출 (추정 데이터의 표시용) ─────────────────
function lineFromTags(tags) {
  const t = (tags || []).find(t => /호선|신분당|수인분당|경의|공항철도|SRT|BRT|GTX/.test(t));
  if (!t) return null;
  // '3호선·수인분당선' 같은 복합 표기는 그대로 사용
  return t.replace(/역$/, '');
}

// ─── 추정 시설 데이터 생성 (facts가 없는 단지용, 결정적) ─────────
function estimateLoc(apt) {
  const h = locHash(apt.id);
  const tags = apt.tags || [];
  const hasSubwayTag = tags.some(t => /호선|신분당|수인분당|경의|공항철도|GTX|역/.test(t));
  const hasSchoolTag = tags.some(t => /학군|학원/.test(t));
  const hasParkTag = tags.some(t => /공원|호수|한강|해변|숲/.test(t));
  const line = lineFromTags(tags);
  // 노선 수: 태그의 '·' 구분 노선 개수 (1km 내 노선수 근사)
  const lines1km = line ? Math.min(3, (line.match(/·/g) || []).length + 1) : 0;
  return {
    // 교통 태그가 있으면 초~준역세권대(250~750m), 없으면 600~1500m
    subway: hasSubwayTag
      ? { name: '최근접 역', line: line || '지하철', dist: 250 + (h % 500), lines1km }
      : { name: '최근접 역', line: line || '지하철', dist: 600 + (h % 900), lines1km },
    hospital: { name: '권역 종합병원', dist: 900 + ((h >> 2) % 1900) },   // 0.9~2.8km
    retail: { name: '최근접 대형마트·백화점', dist: 600 + ((h >> 3) % 2100) }, // 0.6~2.7km
    park: hasParkTag
      ? { name: '인근 공원·수변', dist: 150 + ((h >> 4) % 500) }          // 0.15~0.65km
      : { name: '인근 공원', dist: 300 + ((h >> 4) % 900) },              // 0.3~1.2km
    school: hasSchoolTag
      ? { name: '최근접 초등학교', dist: 200 + ((h >> 5) % 350) }         // 0.2~0.55km
      : { name: '최근접 초등학교', dist: 300 + ((h >> 5) % 600) },        // 0.3~0.9km
    academyCount700: hasSchoolTag ? 40 + (h % 80) : 5 + (h % 25),
    publicFac: { name: '주민센터·도서관', dist: 300 + ((h >> 6) % 700) },
  };
}

// ─── 거리 → 등급(tier) 판정: cuts의 상한 "이하"면 해당 tier ──────
function gradeOf(dist, cuts) {
  for (let i = 0; i < cuts.length; i++) {
    if (dist <= cuts[i][0]) return { tier: i, label: cuts[i][1] };
  }
  // cuts 마지막이 Infinity라 도달 불가하지만 방어적으로 최하 tier 반환
  return { tier: cuts.length - 1, label: cuts[cuts.length - 1][1] };
}

// ─── 학원 밀집도(700m 내 개수) → 0~100 점수 ─────────────────────
function academyScoreOf(count) {
  // 0개=30점(기본 생활권), 100개 이상=100점 (대치·수성급 학원가)
  return Math.min(100, Math.round(30 + count * 0.7));
}

// ─── 종합 진단: 5대 세권 + 3대 환경 + 종합 점수 + 뱃지 ───────────
function diagnoseLocation(apt) {
  const R = LOCATION_RULES;
  const facts = LOC_FACTS[apt.id];
  const loc = facts || estimateLoc(apt);
  const estimated = !facts;

  // ① 5대 세권 등급
  const zones = {
    subway: { ...loc.subway, ...gradeOf(loc.subway.dist, R.subway.cuts) },
    hospital: { ...loc.hospital, ...gradeOf(loc.hospital.dist, R.hospital.cuts) },
    retail: { ...loc.retail, ...gradeOf(loc.retail.dist, R.retail.cuts) },
    park: { ...loc.park, ...gradeOf(loc.park.dist, R.park.cuts) },
    school: { ...loc.school, ...gradeOf(loc.school.dist, R.school.cuts) },
  };

  // ② 3대 환경 점수 (가이드 방법론 준용)
  const ts = R.tierScore;
  // 교통: 최근접 역 등급 중심 + 1km 내 다중 노선 가점 (등급 자체는 미변경 — 점수만 가점)
  const transit = Math.min(100, ts[zones.subway.tier]
    + (loc.subway.lines1km >= 2 ? 4 : 0)
    + (loc.subway.lines1km >= 3 ? 3 : 0));
  // 교육: 초등 접근성(60%) + 학원 밀집도(40%) — "초등 접근성과 학원 밀집 중심" 원문 준용
  const eduAcademy = academyScoreOf(loc.academyCount700);
  const edu = Math.round(ts[zones.school.tier] * 0.6 + eduAcademy * 0.4);
  // 생활: 상권·공원·공공기관 동일 비중 — 원문 준용 (공공기관은 retail cuts로 등급화)
  const pubTier = gradeOf(loc.publicFac.dist, R.retail.cuts).tier;
  const convenience = ts[zones.retail.tier];
  const residential = Math.round((ts[zones.park.tier] + ts[pubTier]) / 2);
  const development = Array.isArray(apt.boons) && apt.boons.length ? 72 : 50;
  const life = Math.round((convenience + residential) / 2);
  // 공개 가중치: 교통 35% · 학군 25% · 상권/편의 20% · 정주여건 10% · 개발호재 10%
  const total = Math.round(transit * 0.35 + edu * 0.25 + convenience * 0.20 + residential * 0.10 + development * 0.10);

  // ③ 뱃지 (등급 tier 기반 — 더블역세권은 표기만, 등급 미반영: 가이드 원문)
  const badges = [];
  if (zones.subway.tier === 0) badges.push('초역세권');
  if (loc.subway.lines1km >= 3) badges.push('트리플역세권');
  else if (loc.subway.lines1km >= 2) badges.push('더블역세권');
  if (zones.school.tier === 0) badges.push('초품아');
  if (zones.park.tier === 0) badges.push('숲세권');
  if (zones.hospital.tier === 0) badges.push('병세권');
  if (zones.retail.tier === 0) badges.push('쇼세권');

  return {
    estimated,                       // true면 UI에 "추정치" 표기 필수
    zones,                           // 5대 세권 {tier,label,dist,name,...}
    env: { transit, edu, life, convenience, residential, development },
    eduAcademyCount: loc.academyCount700,
    publicFac: loc.publicFac,
    total, badges,
    rules: R,                        // UI 범례 렌더링용
  };
}

// ─── 거리 표시 포맷: 1km 미만 m, 이상 km ────────────────────────
function fmtDist(m) {
  // null(반경 내 없음)·Infinity 모두 '반경 밖' 표시
  if (m == null || !isFinite(m)) return '반경 밖';
  return m < 1000 ? m + 'm' : (m / 1000).toFixed(1) + 'km';
}

// ═══ [4단계] 실측 API 연동 — Worker /v1/location + 폴백 ══════════
// 반환: { ...diag, source } — source ∈
//   'live'  : 카카오맵 실측 (Worker 성공)
//   'facts' : 프론트 LOC_FACTS 실측 (API 실패·키 미설정 폴백, 15개 주요 단지)
//   'est'   : 태그 기반 추정 (그 외)
// 메모리 캐시로 같은 세션 내 재호출 방지 (Worker 쪽엔 D1 30일 캐시 별도).
const _locApiCache = {};

async function fetchLocationDiag(apt) {
  // ① 세션 캐시
  if (_locApiCache[apt.id]) return _locApiCache[apt.id];

  // 로컬(폴백) 진단을 먼저 준비 — API 실패 시 즉시 사용
  const local = diagnoseLocation(apt);
  const localOut = { ...local, source: local.estimated ? 'est' : 'facts' };

  // ② API 베이스 결정 (tracker.jsx 의 UJTrack.API_BASE 재사용)
  const API = (typeof window !== 'undefined' && window.UJTrack && window.UJTrack.API_BASE) || '';
  if (!API) { _locApiCache[apt.id] = localOut; return localOut; }

  try {
    const qs = new URLSearchParams({
      apt_id: apt.id, name: apt.name || '',
      region: apt.region || '', dong: apt.dong || '',
    });
    const res = await fetch(API + '/v1/location?' + qs, { signal: AbortSignal.timeout ? AbortSignal.timeout(8000) : undefined });
    const data = await res.json();
    // 키 미설정(503)·좌표 미발견 등 → fallback 신호면 로컬 사용
    if (!res.ok || data.fallback || data.error || !data.zones) {
      _locApiCache[apt.id] = localOut;
      return localOut;
    }
    // 실측 성공 — rules 는 프론트 상수를 붙여 UI 범례 렌더에 사용
    const out = { ...data, rules: LOCATION_RULES, source: 'live' };
    _locApiCache[apt.id] = out;
    return out;
  } catch (e) {
    // 네트워크 오류·타임아웃 → 폴백 (콘솔 노이즈 방지 위해 경고만)
    _locApiCache[apt.id] = localOut;
    return localOut;
  }
}

// ============================================================
// [Task 3-A] 입지 실측점수 → 종합점수 합성 파이프라인
// ------------------------------------------------------------
// 기존 종합점수(가격·거시 12개 요인) 70% + 카카오맵 실측 입지점수 30%.
// 데이터 흐름:
//   ① 지도/상세에서 /v1/location 실측 → D1 30일 캐시 (배치 역할)
//   ② /v1/location/coords 일괄 조회 → applyLocScores()가 APTS에 합성
//   ③ 합성 후 signalOf/gradeOf 로 신호·등급 재판정 → 전 화면 자동 반영
// 실측이 없는 단지는 기존 점수 유지 (혼합 표기 방지 위해 locMeasured 플래그)
// ============================================================
const LOC_BLEND = { base: 0.7, loc: 0.3 }; // 합성 가중치 (승인된 70:30)

function blendScore(baseScore, locTotal) {
  // 반올림 후 0~100 클램프
  return Math.max(0, Math.min(100, Math.round(baseScore * LOC_BLEND.base + locTotal * LOC_BLEND.loc)));
}

/**
 * 캐시된 실측 좌표·입지점수를 일괄 조회해 window.APTS 에 합성.
 * 앱 시작 시 1회 호출 (main.jsx). 완료 후 'urijib:locblend' 이벤트 발행.
 */
async function applyLocScores() {
  try {
    const base = (typeof window !== 'undefined' && window.UJTrack) ? window.UJTrack.API_BASE : null;
    if (!base || !window.APTS) return false;
    const r = await fetch(base + '/v1/location/coords', { cache: 'no-store' });
    if (!r.ok) return false;
    const d = await r.json();
    const coords = d.coords || {};
    let changed = 0;
    window.APTS.forEach((a) => {
      const c = coords[a.id];
      if (!c || c.total == null) return;
      // 원점수 보존 (XAI 툴팁에서 "합성 전/후" 표기용)
      if (a.scoreBase == null) a.scoreBase = a.score;
      a.locTotal = c.total;               // 실측 입지점수
      a.locBadges = c.badges || [];
      a.locMeasured = true;               // 실측 합성 여부 플래그
      a.score = blendScore(a.scoreBase, c.total);
      // 합성 점수로 신호·등급 재판정 (data.jsx 전역 함수)
      if (window.signalOf) a.signal = window.signalOf(a.score, a.delta1y);
      if (window.gradeOf) a.grade = window.gradeOf(a.score);
      changed++;
    });
    if (changed > 0) window.dispatchEvent(new CustomEvent('urijib:locblend', { detail: { changed } }));
    return changed > 0;
  } catch (e) { return false; }
}

// ─── 브라우저 전역 노출 ─────────────────────────────────────────
if (typeof window !== 'undefined') {
  Object.assign(window, { LOCATION_RULES, diagnoseLocation, fetchLocationDiag, fmtDist, locGradeOf: gradeOf, applyLocScores, LOC_BLEND, blendScore });
}

// ─── Node 단위테스트용 export ────────────────────────────────────
if (typeof module !== 'undefined' && module.exports) {
  module.exports = { LOCATION_RULES, LOC_FACTS, gradeOf, academyScoreOf, estimateLoc, diagnoseLocation, fetchLocationDiag, fmtDist, locHash, LOC_BLEND, blendScore };
}
