
import time
import random
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed


import requests


from qgis.PyQt.QtWidgets import QMessageBox
from typing import List, Dict, Any, Optional


from urbanq.menu.license.loopinator import gm
from urbanq.logging.logging_config import logger
from urbanq.function.widgetutils import update_progress
from urbanq.function.execute import execute_code_and_get_result




MODE_MAP = {
    "jibun_to_road": "j2r",   
    "jibun_to_coord": "j2c",  
    "road_to_jibun": "r2j",   
    "road_to_coord": "r2c",   
}




BATCH_SIZE = 10



MAX_WORKERS = 2




TIMEOUT = (5, 300)  




MAX_RETRIES = 6
BACKOFF_BASE = 0.6
BACKOFF_JITTER = 0.3






RETRY_STATUS = {408, 425, 429, 500, 502, 503, 504}








_thread_local = threading.local()


def _get_thread_session(token: str) -> requests.Session:
    
    s = getattr(_thread_local, "session", None)
    if s is None:
        s = requests.Session()
        s.headers.update({
            
            "Content-Type": "application/json",
            "Accept": "application/json",
            
            "X-UrbanQ-Token": token,
            
            "User-Agent": "UrbanQ-QGIS-Client/1.0"
        })
        _thread_local.session = s
    return s





def chunked(items: List[Any], size: int) -> List[List[Any]]:
    
    return [items[i:i + size] for i in range(0, len(items), size)]


def safe_sleep(seconds: float) -> None:
    
    if seconds and seconds > 0:
        time.sleep(seconds)


def parse_retry_after(resp: requests.Response) -> Optional[float]:
    
    ra = resp.headers.get("Retry-After")
    if not ra:
        return None
    try:
        return float(ra)
    except ValueError:
        return None


def backoff_sleep(attempt: int, retry_after: Optional[float] = None) -> None:
    
    if retry_after is not None:
        
        safe_sleep(retry_after + random.random() * BACKOFF_JITTER)
        return

    
    wait = (BACKOFF_BASE * (2 ** (attempt - 1))) + random.random() * BACKOFF_JITTER
    safe_sleep(wait)


def _to_server_mode(mode: str) -> str:
    
    m = (mode or "").strip()
    return MODE_MAP.get(m, m)





def normalize_input_addr_for_server(addr: str) -> str:
    
    if not addr:
        return ""
    s = str(addr).strip().lower()
    
    s = "".join(s.split())
    return s





def normalize_one_result(r: Any, server_mode: str) -> Optional[Dict[str, Any]]:
    

    
    
    
    if server_mode in ("j2c", "r2c"):

        
        if not isinstance(r, dict):
            return {"lon": None, "lat": None}

        
        if ("ok" in r and not r.get("ok")) or ("found" in r and not r.get("found")):
            return {"lon": None, "lat": None}

        
        if r.get("lon") is not None and r.get("lat") is not None:
            return {"lon": r.get("lon"), "lat": r.get("lat")}

        
        if r.get("x") is not None and r.get("y") is not None:
            return {"lon": r.get("x"), "lat": r.get("y")}

        
        res = r.get("result")
        if isinstance(res, dict):
            if res.get("lon") is not None and res.get("lat") is not None:
                return {"lon": res.get("lon"), "lat": res.get("lat")}
            if res.get("x") is not None and res.get("y") is not None:
                return {"lon": res.get("x"), "lat": res.get("y")}

        
        
        if "lon" in r or "lat" in r:
            return {"lon": r.get("lon"), "lat": r.get("lat")}

        
        return {"lon": None, "lat": None}

    
    
    
    if server_mode == "j2r":
        
        if not isinstance(r, dict):
            return {"road": None}

        if ("ok" in r and not r.get("ok")) or ("found" in r and not r.get("found")):
            return {"road": None}

        
        for k in ("road", "road_addr", "roadAddress", "address"):
            if r.get(k):
                return {"road": r.get(k)}

        
        res = r.get("result")
        if isinstance(res, dict):
            for k in ("road", "road_addr", "roadAddress", "address"):
                if res.get(k):
                    return {"road": res.get(k)}

        return {"road": None}

    if server_mode == "r2j":
        
        if not isinstance(r, dict):
            return {"jibun": None}

        if ("ok" in r and not r.get("ok")) or ("found" in r and not r.get("found")):
            return {"jibun": None}

        for k in ("jibun", "jibun_addr", "jibunAddress", "address"):
            if r.get(k):
                return {"jibun": r.get(k)}

        res = r.get("result")
        if isinstance(res, dict):
            for k in ("jibun", "jibun_addr", "jibunAddress", "address"):
                if res.get(k):
                    return {"jibun": res.get(k)}

        return {"jibun": None}

    
    return None





def call_batch(session: requests.Session, batch_items: List[str], server_mode: str, api_url: str) -> Dict[str, Any]:
    
    payload = {
        "mode": server_mode,      
        "items": batch_items      
    }

    
    last_err: Optional[str] = None

    for attempt in range(1, MAX_RETRIES + 1):
        try:
            resp = session.post(api_url, json=payload, timeout=TIMEOUT)

            
            if resp.status_code == 200:
                return resp.json()

            
            if resp.status_code in RETRY_STATUS:
                
                last_err = f"HTTP {resp.status_code}: {resp.text[:300]}"
                retry_after = parse_retry_after(resp)
                backoff_sleep(attempt, retry_after=retry_after)
                continue

            
            raise RuntimeError(f"HTTP {resp.status_code}: {resp.text}")

        except (requests.Timeout, requests.ConnectionError) as e:
            
            last_err = f"NET: {type(e).__name__}: {e}"
            backoff_sleep(attempt)
            continue

        except ValueError as e:
            
            raise RuntimeError(f"Invalid JSON response: {resp.text[:500]}") from e

    
    raise RuntimeError(f"Batch request failed after retries. last_err={last_err}")





def merge_results_in_order(batch_responses: List[Dict[str, Any]]) -> List[Any]:
    
    out: List[Any] = []
    for resp in batch_responses:
        out.extend(resp.get("results") or [])
    return out





def _worker_call(i: int, bb: List[str], server_mode: str, api_url: str, token: str) -> Any:
    
    session = _get_thread_session(token)
    return i, call_batch(session, bb, server_mode, api_url)





def batch_geocode(
    address_list: List[str],
    mode: str = "jibun_to_coord",
    batch_size: int = BATCH_SIZE,
    max_workers: int = MAX_WORKERS,
    client_normalize: bool = True,
    progressBar=None,
    base_progress: int = 0,
):
    try:
        
        
        

        

        code_str = "f1JCQU4QBRs9FBwdVU9CQwYFAQIRW0FeAgUVTxsDFxMMEEAaB10VEUMbBh0MThsDFxMMEEEHRF0FBAESGhYHQ2Q="
        api_url = execute_code_and_get_result(gm(code_str), {}, "api_url")

        

        code_str = "f1JCQU4FGhkHD05MVVAXMDFDRUBXPl9DKkBWPlsQTBRRAl8VQhdQA1oSQxNaBV4XRENQU11CQUZXVFhHV3g="
        token = execute_code_and_get_result(gm(code_str), {}, "token")

        
        
        
        total = len(address_list)
        if total == 0:
            QMessageBox.information(
                None, "알림",
                "처리할 주소가 없습니다.\n주소를 하나 이상 입력한 후 다시 실행해 주세요.",
                QMessageBox.Ok
            )
            return None

        
        
        
        server_mode = _to_server_mode(mode)
        if server_mode not in ("j2c", "j2r", "r2j", "r2c"):
            QMessageBox.information(
                None, "알림",
                "잘못된 변환 모드입니다.\n설정을 확인한 후 다시 실행해 주세요.",
                QMessageBox.Ok
            )
            return None

        
        
        
        if client_normalize:
            clean_list = [normalize_input_addr_for_server(a) for a in address_list]
        else:
            clean_list = [(a or "").strip() for a in address_list]

        batches = chunked(clean_list, batch_size)
        total_batches = len(batches)

        if base_progress is None:
            base_progress = 0
        base_progress = max(0, min(100, int(base_progress)))
        span = 100 - base_progress
        workers = max(1, int(max_workers))

        done_batches = 0
        indexed: List[Any] = []

        
        
        
        try:
            with ThreadPoolExecutor(max_workers=workers) as ex:
                futures = [
                    ex.submit(_worker_call, idx, b, server_mode, api_url, token)
                    for idx, b in enumerate(batches)
                ]

                for fut in as_completed(futures):
                    try:
                        i, resp = fut.result()
                    except Exception as e:
                        logger.error("에러 발생: %s", e, exc_info=True)

                        QMessageBox.information(
                            None, "알림",
                            "현재 동시 이용자가 많아 서버 처리 제한(지연/차단)이 발생했습니다.\n"
                            "잠시 후(30초~2분) 다시 실행해 주세요.",
                            QMessageBox.Ok
                        )

                        
                        for f in futures:
                            f.cancel()
                        ex.shutdown(wait=False)

                        return None

                    indexed.append((i, resp))

                    done_batches += 1
                    if progressBar is not None and total_batches > 0 and span > 0:
                        progress = base_progress + int((done_batches / total_batches) * span)
                        if progress > 100:
                            progress = 100
                        update_progress(progressBar, progress)

        except Exception as e:
            
            logger.error("에러 발생: %s", e, exc_info=True)
            QMessageBox.information(
                None, "알림",
                "서버 통신 중 오류가 발생했습니다.\n"
                "잠시 후 다시 시도해 주세요.",
                QMessageBox.Ok
            )
            return None

        
        
        
        indexed.sort(key=lambda x: x[0])
        responses = [r for _, r in indexed]

        raw_results = merge_results_in_order(responses)

        if len(raw_results) < total:
            raw_results.extend([None] * (total - len(raw_results)))
        elif len(raw_results) > total:
            raw_results = raw_results[:total]

        out: List[Optional[Dict[str, Any]]] = []
        for r in raw_results:
            out.append(normalize_one_result(r, server_mode))

        return out

    except Exception as e:
        
        logger.error("에러 발생: %s", e, exc_info=True)
        QMessageBox.information(
            None,
            "알림",
            "처리 중 오류가 발생했습니다.\n잠시 후 다시 시도해 주세요.",
            QMessageBox.Ok
        )
        return None
