#!/usr/bin/env python3
"""시·군·구·도 지명 점 GeoJSON 만들기 — 앱 지도의 '큰 지명' 자료(rich-rider designer).

돌리는 법(공개용 한 줄 — 이 파일과 입력 파일 4개를 한 폴더에 두고):
  python3 admin_labels.py --src kr_admin_places.json --sup kr_gu_suburbs.json --sup kr_gu_2026.json --bounds 124.3188,32.36076,132.3386,38.64966 --out admin_labels.geojson
  --src·--sup·--bounds 를 안 주면 저장소 경로 기본값(data/ 아래 입력, 앱 tiles.json 의 bounds)을 쓴다.
  --out 은 반드시 준다(기본 출력 경로 없음 — 판마다 코드를 고치거나 검토가 끝난 파일을 덮지 않게).
  쓰는 파일은 --out 하나뿐이고 폴더를 만들지 않는다. 네트워크 0. 파이썬 표준 라이브러리만.

왜: 앱 타일(Geofabrik Shortbread 1.0)의 place_labels 에는 군이 없고(OSM place=county 를 버림), 시는 읍·면과 같은 층·우선순위라
    줌 7~11 에서 면 이름에 밀린다. → 시·군·구·도만 모은 작은 점 파일을 앱에 넣고 맨 위 우선순위로 그린다.

입력(모두 OpenStreetMap, ODbL 1.0)
  --src kr_admin_places.json  osm_admin_places.py 가 Overpass 로 받은 place=county|borough|city|province|state 점.
  --sup (여러 번)             빠진 구 보충. 파일 모양으로 구분해 읽는다.
        kr_gu_suburbs.json    osm_gu_check.py — 7대 도시 place=suburb|borough '…구' 점. suburb 점만 구로 보탠다
                              (부산 서구·동구, 대구 동구 — OSM 에 suburb 로 태그돼 --src 에 없던 구).
        kr_gu_2026.json       osm_gu_relations.py — OSM 에 place 점 없이 행정경계로만 있는 구의 경계 대표점(Nominatim centroid).
                              수원 팔달구, 부천 원미·소사·오정구(2024-01-02), 화성 만세·효행·병점·동탄구(2026-02-01),
                              인천 제물포·영종·서해·검단구(2026-07-01).
  --bounds W,S,E,N 또는 --tiles tiles.json  이 상자 밖 점은 뺀다(땅 없는 바탕 위에 이름만 뜨지 않게 — 남포·송림).
출력: GeoJSON FeatureCollection. 속성 name, rank(0 도 · 1 특별·광역시·특별자치시 · 2 시 · 3 군 · 4 구),
      pop(OSM population, 없으면 0), prio(작을수록 먼저 자리 잡음 = 충돌 시 이김).
규칙
  - 이름에 한글이 있는 점만(상자 안 일본 지명 제외). 북한 지명도 OSM 그대로(앱 타일이 휴전선 북쪽 일부 포함).
  - 같은 이름·같은 rank 가 15km 안에 두 번이면 인구값 있는 점 하나만(군청 점 + 옛 중심 점 — 해남·하동·구례).
  - 도는 place=province 만. place=state 는 이 상자 안에서 북한 3점(강원도·황해남도·황해북도)뿐이라 뺀다.
  - 구(place=borough)는 이름이 '구'로 끝나는 것만. '○○구청' → '○○구'(대구 중구는 OSM 에 '중구청' 점뿐). '신봉동' 같은 건 뺀다.
  - 2026-10-14 기준 공식 이름이 아닌 점: RETIRE 표는 빼고, RENAME 표는 OSM 현재 이름으로 바꾼다(표마다 공식 출처 URL).
    새 이름의 점은 손으로 찍지 않는다 — --sup 의 OSM 점으로만 들어온다. OSM 에 없으면 빈칸(틀린 이름보다 빈칸).
OSM 데이터 © OpenStreetMap 기여자, ODbL 1.0 https://opendatacommons.org/licenses/odbl/1-0/
"""
import argparse
import hashlib
import json
import math
import os
import re

HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.abspath(os.path.join(HERE, '..', '..', '..'))
DEF_SRC = os.path.join(HERE, 'data', 'kr_admin_places.json')
DEF_SUP = [os.path.join(HERE, 'data', 'kr_gu_suburbs.json'), os.path.join(HERE, 'data', 'kr_gu_2026.json')]
DEF_TILES = os.path.join(ROOT, 'app', 'android', 'app', 'src', 'main', 'assets', 'map', 'tiles.json')
ODBL = 'https://opendatacommons.org/licenses/odbl/1-0/'
HANGUL = re.compile('[가-힣]')
DUP_KM = 15  # 같은 이름 점 하나로 합치는 거리(시·군 중심끼리는 가장 가까운 곳도 15km 넘게 떨어짐)

INCHEON = 'https://www.incheon.go.kr/IC010601/2187729'
JEONNAM_GWANGJU = 'https://www.yna.co.kr/view/AKR20260630109800054'
# 폐지된 행정구역의 OSM 점(--src 의 OSM node id). OSM 의 이 점들은 2026-10-14 에도 옛 이름이라(api.openstreetmap.org 재확인)
# 이름 규칙으로는 못 거른다. 같은 자리의 새 이름은 --sup 로만 들어온다.
RETIRE = {
    '415154839': ('인천 중구', '2026-07-01 폐지 → 제물포구·영종구', INCHEON),
    '9364653292': ('인천 동구', '2026-07-01 폐지 → 제물포구', INCHEON),
    '415153385': ('인천 서구', '2026-07-01 서해구·검단구로 개편', INCHEON),
    '13439525610': ('전라남도', '2026-07-01 폐지 → 전남광주통합특별시. 그 이름의 OSM place 점 없음 → 빈칸', JEONNAM_GWANGJU),
}
# 이름만 바뀐 점: OSM node id → (새 이름 = 2026-10-14 OSM 현재 name 태그, rank, 사유, 출처)
RENAME = {
    '358015168': ('광주', 1, '광주광역시 2026-07-01 폐지(전남광주통합특별시, 약칭 광주특별시). OSM node 현재 name=광주(v68, 2026-07-06)',
                  JEONNAM_GWANGJU),
}


def fix_name(place, name):
    """구 점 이름 정리. '○○구청' → '○○구'. 그 밖의 이름은 그대로."""
    if place == 'borough' and name.endswith('구청'):
        return name[:-1]
    return name


def rank(place, name):
    if place == 'state':
        return None
    if place == 'province':
        return 0
    if name.endswith(('특별시', '광역시', '특별자치시')):
        return 1
    if place == 'city':
        return 2
    if place == 'county':
        return 3
    if place == 'borough':
        return 4 if name.endswith('구') else None
    return None


def pop_int(v):
    try:
        return int(re.sub(r'[^0-9]', '', str(v))) if v else 0
    except ValueError:
        return 0


def km(a, b):
    dx = (a[0] - b[0]) * 111.32 * math.cos(math.radians((a[1] + b[1]) / 2))
    dy = (a[1] - b[1]) * 110.57
    return math.hypot(dx, dy)


def read_sup(path, log):
    """보충 파일 하나 → 구 점 목록. 이미 있는 구(같은 이름·15km 안)는 DUP_KM 규칙이 걸러 두 번 들어가지 않는다."""
    sup = json.load(open(path))
    base = os.path.basename(path)
    out = []
    if 'nodes' in sup:  # osm_gu_check.py 모양
        for nid, n in sorted(sup['nodes'].items()):
            if n['place'] != 'suburb':
                continue
            out.append({'id': 'n' + nid, 'name': n['name'], 'place': 'borough', 'population': None,
                        'lon': n['lon'], 'lat': n['lat']})
            log.append(f'  보충({base}, OSM place=suburb → 구): {n["name"]} {n["city"]} node {nid} ({n["lat"]:.4f},{n["lon"]:.4f})')
    elif 'gu' in sup:  # osm_gu_relations.py 모양
        for g in sup['gu']:
            out.append({'id': f'r{g["relation"]}', 'name': g['name'], 'place': 'borough', 'population': None,
                        'lon': g['lon'], 'lat': g['lat']})
            log.append(f'  보충({base}, OSM 행정경계 대표점 → 구): {g["name"]} {g["city"]} relation {g["relation"]} '
                       f'({g["lat"]:.4f},{g["lon"]:.4f}) 출처 {g["official"]}')
        for m in sup.get('missing', []):
            log.append(f'  빈칸({base}, OSM 에서 못 찾음): {m["name"]} {m["city"]} — {m["why"]}')
    else:
        raise SystemExit(f'보충 파일 모양을 모름: {path}')
    return out, sup.get('fetched')


def main():
    ap = argparse.ArgumentParser(description='시·군·구·도 지명 점 GeoJSON (OSM, ODbL 1.0)')
    ap.add_argument('--src', default=DEF_SRC, help='kr_admin_places.json')
    ap.add_argument('--sup', action='append', help='보충 파일(여러 번). 안 주면 저장소 data/ 의 2개')
    b = ap.add_mutually_exclusive_group()
    b.add_argument('--bounds', help='W,S,E,N')
    b.add_argument('--tiles', help='bounds 를 읽을 tiles.json. 둘 다 없으면 저장소 앱 tiles.json')
    ap.add_argument('--out', required=True, help='출력 GeoJSON(쓰는 파일은 이것 하나)')
    a = ap.parse_args()
    if a.bounds:
        w, so, e, no = [float(x) for x in a.bounds.split(',')]
        bsrc = '--bounds'
    else:
        w, so, e, no = json.load(open(a.tiles or DEF_TILES))['bounds']
        bsrc = a.tiles or os.path.relpath(DEF_TILES, ROOT)

    d = json.load(open(a.src))
    log = []
    nodes = [dict(n, id=k) for k, n in d['nodes'].items()]
    fetched = [f'{os.path.basename(a.src)} {d.get("fetched")}']
    for p in a.sup or DEF_SUP:
        pts, fe = read_sup(p, log)
        nodes += pts
        fetched.append(f'{os.path.basename(p)} {fe}')

    seen = []
    skipped = {'no_hangul': 0, 'dup': 0, 'other': 0, 'state': 0, 'not_gu': 0, 'out_of_bounds': 0, 'retired': 0}
    for n in nodes:
        raw = (n.get('name') or '').strip()
        if n['id'] in RETIRE:
            label, why, url = RETIRE[n['id']]
            skipped['retired'] += 1
            log.append(f'  뺌(공식 이름 아님): {raw} = {label} node {n["id"]} ({n["lat"]:.4f},{n["lon"]:.4f}) — {why} 출처 {url}')
            continue
        if not HANGUL.search(raw):
            skipped['no_hangul'] += 1
            continue
        name = fix_name(n['place'], raw)
        r = rank(n['place'], name)
        if n['id'] in RENAME:
            new, r, why, url = RENAME[n['id']]
            log.append(f'  이름 고침(공식 개편): {raw} → {new} node {n["id"]} — {why} 출처 {url}')
            name = new
        elif name != raw:
            log.append(f'  이름 고침: {raw} → {name} ({n["lat"]:.3f},{n["lon"]:.3f})')
        if r is None:
            key = {'state': 'state', 'borough': 'not_gu'}.get(n['place'], 'other')
            skipped[key] += 1
            if key != 'other':
                log.append(f'  뺌({key}): {raw} place={n["place"]} ({n["lat"]:.3f},{n["lon"]:.3f})')
            continue
        if not (w <= n['lon'] <= e and so <= n['lat'] <= no):
            skipped['out_of_bounds'] += 1
            log.append(f'  뺌(타일 bounds 밖): {name} rank {r} ({n["lat"]:.3f},{n["lon"]:.3f})')
            continue
        pt = (n['lon'], n['lat'])
        p = pop_int(n.get('population'))
        twin = next((i for i, s in enumerate(seen) if s[0] == name and s[1] == r and km(s[2], pt) < DUP_KM), None)
        if twin is not None:
            skipped['dup'] += 1
            if p > seen[twin][3]:
                seen[twin] = (name, r, pt, p)
            continue
        seen.append((name, r, pt, p))

    feats = []
    for name, r, pt, p in seen:
        # 우선순위: 특별·광역시 → (시·군 함께) 인구 큰 순 → 인구 모르는 시·군 → 구 → 도(넓게 볼 때만 따로 그림)
        if r == 1:
            prio = 0
        elif r in (2, 3):
            prio = 1 + (10_000_000 - min(p, 9_999_999)) if p else 20_000_000
        elif r == 4:
            prio = 30_000_000 + (10_000_000 - min(p, 9_999_999) if p else 10_000_000)
        else:
            prio = 50_000_000
        feats.append({'type': 'Feature', 'properties': {'name': name, 'rank': r, 'pop': p, 'prio': prio},
                      'geometry': {'type': 'Point', 'coordinates': [round(pt[0], 5), round(pt[1], 5)]}})
    feats.sort(key=lambda f: f['properties']['prio'])
    out = {'type': 'FeatureCollection',
           'note': 'OSM place 점·행정경계 대표점(도·특별/광역시·시·군·구). © OpenStreetMap 기여자, ODbL 1.0 ' + ODBL
                   + ' — 입력 조회 시각: ' + '; '.join(fetched) + '. admin_labels.py 가 만듦 — 손으로 고치지 말 것.',
           'features': feats}
    with open(a.out, 'w') as f:  # 쓰는 파일은 이것 하나. 폴더를 만들지 않는다.
        json.dump(out, f, ensure_ascii=False, separators=(',', ':'))

    by = {}
    for f in feats:
        by.setdefault(f['properties']['rank'], []).append(f['properties'])
    names = {0: '도', 1: '특별·광역시', 2: '시', 3: '군', 4: '구'}
    print(f'입력 {os.path.basename(a.src)} {len(d["nodes"])}점 + 보충 {len(nodes) - len(d["nodes"])}점 '
          f'→ 지명 {len(feats)}개, 건너뜀 {skipped}')
    print(f'bounds {[w, so, e, no]} ({bsrc})')
    print('\n'.join(sorted(log)))
    body = open(a.out, 'rb').read()
    print(f'파일 {a.out} {len(body):,} B sha256 {hashlib.sha256(body).hexdigest()}')
    for r in sorted(by):
        ps = by[r]
        print(f'  rank {r} {names[r]}: {len(ps)}개 (인구값 있음 {sum(1 for p in ps if p["pop"])}) — '
              + ', '.join(p['name'] for p in ps))


if __name__ == '__main__':
    main()
