import bpy
import webbrowser
import requests
from typing import Optional
import os
import tempfile
import mimetypes
import time
import uuid
import threading
import queue
import locale as _locale
from urllib.parse import urlsplit, urlunsplit, parse_qs
from . import tos

_ACTIVE_POLLERS = set()
_ACTIVE_POLLERS_LOCK = threading.Lock()

_REQ_TIMEOUT = (10, 60)
_REQ_TIMEOUT_POST = (10, 120)
_MAX_IMG_SIZE = 20 * 1024 * 1024

def _build_headers(authorization: str):
    lang = (_locale.getlocale()[0] or "en_US").replace("_", "-")
    return {
        "Authorization": authorization,
        "User-Agent": "Apifox/1.0.0 (https://apifox.com)",
        "Accept": "*/*",
        "Host": "api.hitem3d.ai",
        "Connection": "keep-alive",
        "Accept-Language": lang,
    }

_MAIN_THREAD_QUEUE = queue.Queue()
_MAIN_THREAD_PUMP_LOCK = threading.Lock()
_MAIN_THREAD_PUMP_STARTED = False
_MAIN_THREAD_PUMP_LAST_TICK = 0.0
_MAIN_THREAD_PUMP_WATCHDOG = False

def _ensure_main_thread_pump():
    global _MAIN_THREAD_PUMP_STARTED, _MAIN_THREAD_PUMP_LAST_TICK
    now = time.time()
    with _MAIN_THREAD_PUMP_LOCK:
        if _MAIN_THREAD_PUMP_STARTED and (now - _MAIN_THREAD_PUMP_LAST_TICK) < 1.0:
            return
        _MAIN_THREAD_PUMP_STARTED = True
    def _pump():
        global _MAIN_THREAD_PUMP_LAST_TICK
        _MAIN_THREAD_PUMP_LAST_TICK = time.time()
        processed = 0
        while processed < 50:
            try:
                fn = _MAIN_THREAD_QUEUE.get_nowait()
            except queue.Empty:
                break
            try:
                fn()
            except Exception as e:
                print(f"Main thread task failed: {e}")
            processed += 1
        return 0.1
    try:
        bpy.app.timers.register(_pump, first_interval=0.1, persistent=True)
    except Exception as e:
        with _MAIN_THREAD_PUMP_LOCK:
            _MAIN_THREAD_PUMP_STARTED = False
        print(f"Failed to start main thread pump: {e}")
        def _retry():
            try:
                _ensure_main_thread_pump()
            except Exception:
                pass
        try:
            if not _MAIN_THREAD_PUMP_WATCHDOG:
                _MAIN_THREAD_PUMP_WATCHDOG = True
                t = threading.Timer(1.0, _retry)
                t.daemon = True
                t.start()
        except Exception:
            pass

def _post_to_main_thread(fn):
    try:
        if threading.current_thread() is threading.main_thread():
            try:
                fn()
            except Exception as e:
                print(f"Main thread direct exec failed: {e}")
        else:
            _MAIN_THREAD_QUEUE.put(fn)
            _ensure_main_thread_pump()
    except Exception:
        try:
            _MAIN_THREAD_QUEUE.put(fn)
            _ensure_main_thread_pump()
        except Exception as e:
            print(f"Fallback enqueue failed: {e}")

def ensure_ui_pump():
    _ensure_main_thread_pump()

def on_blend_load():
    global _MAIN_THREAD_PUMP_STARTED, _MAIN_THREAD_PUMP_LAST_TICK
    with _MAIN_THREAD_PUMP_LOCK:
        _MAIN_THREAD_PUMP_STARTED = False
        _MAIN_THREAD_PUMP_LAST_TICK = 0.0
    with _ACTIVE_POLLERS_LOCK:
        _ACTIVE_POLLERS.clear()
    for scn in getattr(bpy.data, "scenes", []):
        try:
            scn.hi3d_is_generating = False
        except Exception:
            pass
        try:
            scn.hi3d_is_generating_texture = False
        except Exception:
            pass
        try:
            scn.hi3d_is_replacing_texture = False
        except Exception:
            pass
    try:
        while True:
            _MAIN_THREAD_QUEUE.get_nowait()
    except queue.Empty:
        pass
    _ensure_main_thread_pump()

def _schedule_update_gen_status(scene_name: str, plug_task_id: str, status: str):
    def f():
        scene = bpy.data.scenes.get(scene_name)
        tasks = getattr(scene, "hi3d_tasks", None) if scene is not None else None
        if tasks is not None:
            for i in range(len(tasks)):
                if tasks[i].task_id == plug_task_id:
                    tasks[i].gen_status = status
                    break
        return None
    _post_to_main_thread(f)

def _schedule_update_tex_status(scene_name: str, plug_task_id: str, create_time: str, status: str):
    def f():
        scene = bpy.data.scenes.get(scene_name)
        tasks = getattr(scene, "hi3d_texture_tasks", None) if scene is not None else None
        if tasks is not None:
            for i in range(len(tasks)):
                if tasks[i].task_id == plug_task_id and tasks[i].create_time == create_time:
                    tasks[i].tex_status = status
                    break
        return None
    _post_to_main_thread(f)

def _schedule_delete_gen_task(scene_name: str, plug_task_id: str):
    def f():
        scene = bpy.data.scenes.get(scene_name)
        tasks = getattr(scene, "hi3d_tasks", None) if scene is not None else None
        if tasks is not None:
            for i in range(len(tasks)):
                if tasks[i].task_id == plug_task_id:
                    tasks.remove(i)
                    break
        return None
    _post_to_main_thread(f)

def _schedule_delete_tex_task(scene_name: str, plug_task_id: str, create_time: str):
    def f():
        scene = bpy.data.scenes.get(scene_name)
        tasks = getattr(scene, "hi3d_texture_tasks", None) if scene is not None else None
        if tasks is not None:
            for i in range(len(tasks)):
                if tasks[i].task_id == plug_task_id and tasks[i].create_time == create_time:
                    tasks.remove(i)
                    break
        return None
    _post_to_main_thread(f)

def _poll_status(status_url: str, get_headers, on_state):
    last_state = None
    for _ in range(720):
        try:
            headers = get_headers()
        except Exception as e:
            print(f"Get headers failed: {e}")
            return {"ok": False, "last_state": last_state, "reason": "get_headers"}
        try:
            resp = requests.get(status_url, headers=headers, timeout=_REQ_TIMEOUT)
            try:
                print(resp.text)
            except Exception:
                pass
        except Exception as e:
            print(f"Failed to get status: {e}")
            time.sleep(5)
            continue
        try:
            obj = resp.json()
        except Exception:
            time.sleep(5)
            continue
        if obj.get("code") != 200:
            return {"ok": False, "last_state": last_state, "reason": "api_code"}
        data_obj = obj.get("data") or {}
        state = data_obj.get("state")
        last_state = state
        cont = False
        try:
            cont = on_state(state, data_obj)
        except Exception as e:
            print(f"Handle state error: {e}")
            return {"ok": False, "last_state": last_state, "reason": "handle_state"}
        if not cont:
            return {"ok": True, "last_state": last_state, "reason": "terminal"}
        time.sleep(5)
    return {"ok": False, "last_state": last_state, "reason": "timeout"}

def _show_error_dialog(message: str):
    try:
        bpy.ops.hi3d.show_error('INVOKE_DEFAULT', message=message)
    except Exception:
        print(f"[Error] {message}")

def open_url(url: str):
    try:
        bpy.ops.wm.url_open(url=url)
    except Exception:
        webbrowser.open(url)


RES_LIMITS = {
    "512": 500000,
    "1536": 2000000,
    "1536fast": 2000000,
    "1536pro": 2000000,
}

def get_face_limit_max(scene: Optional[bpy.types.Scene]) -> int:
    if not scene:
        return 2000000
    res = getattr(scene, "hi3d_resolution", "1536fast")
    return RES_LIMITS.get(res, 2000000)

def generation_worker(
    scene_name: str,
    authorization: str,
    task_type: str,
    uid: str,
    resolution: str,
    face: int,
    model_type: str,
    model_version: str,
    pic_path: str,
    front_path: str,
    back_path: str,
    left_path: str,
    right_path: str,
):
    if not authorization:
        _post_to_main_thread(lambda: _show_error_dialog("Authorization is empty! Please confirm API keys."))
        _schedule_update_gen_status(scene_name, uid, "FAIL")
        return

    url = "https://api.hitem3d.ai/open-api/v1/submit-task"
    headers = _build_headers(authorization)
    try:
        f_int = int(face)
    except Exception:
        f_int = 2000000
    f_int = max(100000, min(2000000, f_int))
    f_int = (f_int // 10000) * 10000

    if model_type == "GENERAL":
        if model_version == "v2_1":
            model = "hitem3dv2.1"
        elif model_version == "v2_0":
            model = "hitem3dv2.0"
        elif model_version == "v1_5":
            model = "hitem3dv1.5"
        else:
            model = "hitem3dv2.1"
    else:
        if model_version == "v2_1":
            model = "scene-portraitv2.1"
        elif model_version == "v2_0":
            model = "scene-portraitv2.0"
        elif model_version == "v1_5":
            model = "scene-portraitv1.5"
        else:
            model = "scene-portraitv2.0"

    data = {
        "request_type": "1",
        "resolution": resolution,
        "face": f_int,
        "model": model,
        "format": "4",
        "plug_type": "BLENDER",
        "plug_task_id": uid,
    }

    opened = []
    try:
        files = None
        if task_type == "image_to_model":
            try:
                if pic_path and os.path.exists(pic_path) and os.path.getsize(pic_path) > _MAX_IMG_SIZE:
                    _schedule_delete_gen_task(scene_name, uid)
                    _post_to_main_thread(lambda: _show_error_dialog("Input image is larger than 20MB. Please use a smaller image."))
                    return
            except Exception:
                pass
            try:
                fh = open(pic_path, "rb")
            except Exception as e:
                _post_to_main_thread(lambda m=str(e): _show_error_dialog(f"Failed to open input image: {m}"))
                _schedule_update_gen_status(scene_name, uid, "FAIL")
                return
            opened.append(fh)
            files = {"images": ("input.jpg", fh, "image/jpeg")}
        else:
            multi_files = []
            multi_images_bit = ""
            for p, name in [
                (front_path, "front.jpg"),
                (back_path, "back.jpg"),
                (left_path, "left.jpg"),
                (right_path, "right.jpg"),
            ]:
                if p:
                    try:
                        if os.path.exists(p) and os.path.getsize(p) > _MAX_IMG_SIZE:
                            _schedule_delete_gen_task(scene_name, uid)
                            _post_to_main_thread(lambda n=name: _show_error_dialog(f"Multi-view image {n} is larger than 20MB. Please replace it with a smaller image."))
                            return
                    except Exception:
                        pass
                    try:
                        fh = open(p, "rb")
                    except Exception as e:
                        _post_to_main_thread(lambda m=str(e): _show_error_dialog(f"Failed to open multi-view image: {m}"))
                        _schedule_update_gen_status(scene_name, uid, "FAIL")
                        return
                    opened.append(fh)
                    multi_files.append(("multi_images", (name, fh, "image/jpeg")))
                    multi_images_bit += "1"
                else:
                    multi_images_bit += "0"
            if not multi_files:
                _post_to_main_thread(lambda: _show_error_dialog("No images selected for multi-view"))
                _schedule_update_gen_status(scene_name, uid, "FAIL")
                return
            data["multi_images_bit"] = multi_images_bit
            files = multi_files

        resp = requests.post(url, headers=headers, files=files, data=data, timeout=_REQ_TIMEOUT_POST)
        try:
            submit_body = resp.json()
        except Exception:
            _post_to_main_thread(lambda: _show_error_dialog("Failed to parse submit response body."))
            _schedule_update_gen_status(scene_name, uid, "FAIL")
            return
        if submit_body.get("code") != 200:
            msg = submit_body.get("msg") or resp.text or "Unknown error"
            low = str(msg).lower()
            if "expired" in low or "login expired" in low or "token" in low and "expired" in low:
                scn = bpy.data.scenes.get(scene_name)
                new_auth = refresh_authorization_from_scene(scene_name)
                if new_auth:
                    headers = _build_headers(new_auth)
                    resp = requests.post(url, headers=headers, files=files, data=data, timeout=_REQ_TIMEOUT_POST)
                    try:
                        submit_body = resp.json()
                    except Exception:
                        _post_to_main_thread(lambda: _show_error_dialog("Failed to parse submit response body."))
                        _schedule_update_gen_status(scene_name, uid, "FAIL")
                        return
                    if submit_body.get("code") != 200:
                        msg = submit_body.get("msg") or resp.text or "Unknown error"
                        _post_to_main_thread(lambda m=msg: _show_error_dialog(m))
                        _schedule_update_gen_status(scene_name, uid, "FAIL")
                        return
                else:
                    _post_to_main_thread(lambda m=msg: _show_error_dialog(m))
                    _schedule_update_gen_status(scene_name, uid, "FAIL")
                    return
            else:
                _post_to_main_thread(lambda m=msg: _show_error_dialog(m))
                _schedule_update_gen_status(scene_name, uid, "FAIL")
                return
        api_task_id = (submit_body.get("data") or {}).get("task_id") or ""
        if not api_task_id:
            _post_to_main_thread(lambda: _show_error_dialog("Failed to submit task: missing task_id in response."))
            _schedule_update_gen_status(scene_name, uid, "FAIL")
            return

        def _update_api_task_id():
            scene = bpy.data.scenes.get(scene_name)
            tasks = getattr(scene, "hi3d_tasks", None) if scene is not None else None
            if tasks is not None:
                for i in range(len(tasks)):
                    if tasks[i].task_id == uid:
                        tasks[i].api_task_id = api_task_id
                        if getattr(tasks[i], "gen_status", "") == "INIT":
                            tasks[i].gen_status = "QUEUE"
                        break
            return None
        _post_to_main_thread(_update_api_task_id)

        try:
            bal_url = "https://api.hitem3d.ai/open-api/v1/auth/balance"
            bal_resp = requests.get(bal_url, headers=_build_headers(authorization), timeout=_REQ_TIMEOUT)
            bal_body = bal_resp.json()
            if isinstance(bal_body, dict) and bal_body.get("code") == 200:
                bal = ((bal_body.get("data") or {}).get("totalBalance") or 0)
                _post_to_main_thread(lambda b=float(bal): setattr(bpy.data.scenes.get(scene_name), "hi3d_balance", b) if bpy.data.scenes.get(scene_name) else None)
        except Exception:
            pass

        _start_generation_poller(scene_name, uid, api_task_id, authorization)
    except Exception as e:
        _post_to_main_thread(lambda m=str(e): _show_error_dialog(f"Generation request failed: {m}"))
        _schedule_update_gen_status(scene_name, uid, "FAIL")
    finally:
        for fh in opened:
            try:
                fh.close()
            except Exception:
                pass

def texture_worker(scene_name: str, authorization: str, glb_path: str, image_path: str, plug_task_id: str, create_time: str):
    if not authorization:
        _post_to_main_thread(lambda: _show_error_dialog("Authorization is empty! Please confirm API keys."))
        _schedule_update_tex_status(scene_name, plug_task_id, create_time, "FAIL")
        if os.path.exists(glb_path):
            os.remove(glb_path)
        return
    try:
        if image_path and os.path.exists(image_path) and os.path.getsize(image_path) > _MAX_IMG_SIZE:
            _schedule_delete_tex_task(scene_name, plug_task_id, create_time)
            _post_to_main_thread(lambda: _show_error_dialog("Texture image is larger than 20MB. Please use a smaller image."))
            if os.path.exists(glb_path):
                os.remove(glb_path)
            return
    except Exception:
        pass

    token_url = "https://api.hitem3d.ai/open-api/v1/upload/token"
    try:
        response = requests.get(token_url, headers=_build_headers(authorization), timeout=_REQ_TIMEOUT)
        body = response.json()
    except Exception as e:
        if os.path.exists(glb_path):
            os.remove(glb_path)
        _post_to_main_thread(lambda m=str(e): _show_error_dialog(f"Failed to get upload token: {m}"))
        _schedule_update_tex_status(scene_name, plug_task_id, create_time, "FAIL")
        return
    if body.get("code") != 200:
        msg = body.get("message", "Unknown error")
        low = str(msg).lower()
        if "expired" in low or "login expired" in low or ("token" in low and "expired" in low):
            new_auth = refresh_authorization_from_scene(scene_name)
            if new_auth:
                try:
                    response = requests.get(token_url, headers=_build_headers(new_auth), timeout=_REQ_TIMEOUT)
                    body = response.json()
                except Exception as e:
                    if os.path.exists(glb_path):
                        os.remove(glb_path)
                    _post_to_main_thread(lambda m=str(e): _show_error_dialog(f"Failed to get upload token: {m}"))
                    _schedule_update_tex_status(scene_name, plug_task_id, create_time, "FAIL")
                    return
                if body.get("code") != 200:
                    msg = body.get("message", "Unknown error")
                    if os.path.exists(glb_path):
                        os.remove(glb_path)
                    _post_to_main_thread(lambda m=msg: _show_error_dialog(f"Failed to get upload token: {m}"))
                    _schedule_update_tex_status(scene_name, plug_task_id, create_time, "FAIL")
                    return
            else:
                if os.path.exists(glb_path):
                    os.remove(glb_path)
                _post_to_main_thread(lambda m=msg: _show_error_dialog(f"Failed to get upload token: {m}"))
                _schedule_update_tex_status(scene_name, plug_task_id, create_time, "FAIL")
                return
        else:
            if os.path.exists(glb_path):
                os.remove(glb_path)
            _post_to_main_thread(lambda m=msg: _show_error_dialog(f"Failed to get upload token: {m}"))
            _schedule_update_tex_status(scene_name, plug_task_id, create_time, "FAIL")
            return

    data = body.get("data", {}) if isinstance(body, dict) else {}
    ak = data.get("accessKeyId", None)
    sk = data.get("secretAccessKey", None)
    sessionKey = data.get("sessionKey", None)
    endpoint = "tos-ap-southeast-1.volces.com"
    region = "ap-southeast-1"
    bucket_name = "mm-sparc3d-prod"
    obj_id = str(uuid.uuid4())
    object_key = f"blender/mesh/{obj_id}.glb"

    try:
        client = tos.TosClientV2(ak, sk, endpoint, region, security_token=sessionKey)
        client.put_object_from_file(bucket_name, object_key, glb_path)
    except Exception as e:
        if os.path.exists(glb_path):
            os.remove(glb_path)
        _post_to_main_thread(lambda m=str(e): _show_error_dialog(f"Failed to upload mesh: {m}"))
        _schedule_update_tex_status(scene_name, plug_task_id, create_time, "FAIL")
        return
    finally:
        if os.path.exists(glb_path):
            os.remove(glb_path)

    submit_url = "https://api.hitem3d.ai/open-api/v1/submit-task"
    headers = _build_headers(authorization)
    mesh_url = "https://hitem3dstatic.zaohaowu.net/" + object_key
    submit_data = {
        "request_type": "2",
        "model": "hitem3dv1.5",
        "format": "4",
        "face": "500000",
        "resolution": "512",
        "mesh_url": mesh_url,
        "plug_type": "BLENDER",
        "plug_task_id": plug_task_id,
    }

    image_file = None
    try:
        image_file = open(image_path, "rb")
        files = {"images": ("texture.jpg", image_file, "image/jpeg")}
        resp = requests.post(submit_url, headers=headers, files=files, data=submit_data, timeout=_REQ_TIMEOUT_POST)
        if resp.status_code != 200:
            _post_to_main_thread(lambda m=f"HTTP {resp.status_code}": _show_error_dialog(f"Texture generation failed: {m}"))
            _schedule_update_tex_status(scene_name, plug_task_id, create_time, "FAIL")
            return
        try:
            body = resp.json()
        except Exception:
            _post_to_main_thread(lambda: _show_error_dialog("Failed to parse response body."))
            _schedule_update_tex_status(scene_name, plug_task_id, create_time, "FAIL")
            return
        if body.get("code") != 200:
            msg = body.get("msg") or "Unknown error"
            low = str(msg).lower()
            if "expired" in low or "login expired" in low or ("token" in low and "expired" in low):
                new_auth = refresh_authorization_from_scene(scene_name)
                if new_auth:
                    headers = _build_headers(new_auth)
                    resp = requests.post(submit_url, headers=headers, files=files, data=submit_data, timeout=_REQ_TIMEOUT_POST)
                    if resp.status_code != 200:
                        _post_to_main_thread(lambda m=f"HTTP {resp.status_code}": _show_error_dialog(f"Texture generation failed: {m}"))
                        _schedule_update_tex_status(scene_name, plug_task_id, create_time, "FAIL")
                        return
                    try:
                        body = resp.json()
                    except Exception:
                        _post_to_main_thread(lambda: _show_error_dialog("Failed to parse response body."))
                        _schedule_update_tex_status(scene_name, plug_task_id, create_time, "FAIL")
                        return
                    if body.get("code") != 200:
                        msg = body.get("msg") or "Unknown error"
                        _post_to_main_thread(lambda m=msg: _show_error_dialog(m))
                        _schedule_update_tex_status(scene_name, plug_task_id, create_time, "FAIL")
                        return
                else:
                    _post_to_main_thread(lambda m=msg: _show_error_dialog(m))
                    _schedule_update_tex_status(scene_name, plug_task_id, create_time, "FAIL")
                    return
            else:
                _post_to_main_thread(lambda m=msg: _show_error_dialog(m))
                _schedule_update_tex_status(scene_name, plug_task_id, create_time, "FAIL")
                return
        api_task_id = (body.get("data") or {}).get("task_id") or ""
        if not api_task_id:
            _post_to_main_thread(lambda: _show_error_dialog("Texture generation failed: missing task_id in response."))
            _schedule_update_tex_status(scene_name, plug_task_id, create_time, "FAIL")
            return

        def _update_api_task_id():
            scene = bpy.data.scenes.get(scene_name)
            tasks = getattr(scene, "hi3d_texture_tasks", None) if scene is not None else None
            if tasks is not None:
                for i in range(len(tasks)):
                    if tasks[i].task_id == plug_task_id and tasks[i].create_time == create_time:
                        tasks[i].api_task_id = api_task_id
                        if getattr(tasks[i], "tex_status", "") == "INIT":
                            tasks[i].tex_status = "QUEUE"
                        break
            return None
        _post_to_main_thread(_update_api_task_id)

        try:
            bal_url = "https://api.hitem3d.ai/open-api/v1/auth/balance"
            bal_resp = requests.get(bal_url, headers=_build_headers(authorization), timeout=_REQ_TIMEOUT)
            bal_body = bal_resp.json()
            if isinstance(bal_body, dict) and bal_body.get("code") == 200:
                bal = ((bal_body.get("data") or {}).get("totalBalance") or 0)
                _post_to_main_thread(lambda b=float(bal): setattr(bpy.data.scenes.get(scene_name), "hi3d_balance", b) if bpy.data.scenes.get(scene_name) else None)
        except Exception:
            pass

        _start_texture_poller(scene_name, plug_task_id, create_time, api_task_id, authorization)
    except Exception as e:
        _post_to_main_thread(lambda m=str(e): _show_error_dialog(f"Texture generation failed: {m}"))
        _schedule_update_tex_status(scene_name, plug_task_id, create_time, "FAIL")
    finally:
        if image_file is not None:
            try:
                image_file.close()
            except Exception:
                pass

def refresh_authorization_from_scene(scene_name: str) -> str | None:
    scene = bpy.data.scenes.get(scene_name)
    if scene is None:
        return None
    ak = getattr(scene, "hi3d_access_key", "") or ""
    sk = getattr(scene, "hi3d_secret_key", "") or ""
    if not ak or not sk:
        return None
    try:
        import base64
        token = f"{ak}:{sk}".encode("utf-8")
        auth = "Basic " + base64.b64encode(token).decode("ascii")
        url = "https://api.hitem3d.ai/open-api/v1/auth/token"
        headers = {
           'Authorization': auth,
           'User-Agent': 'Apifox/1.0.0 (https://apifox.com)',
           'Content-Type': 'application/json',
           'Accept': '*/*',
           'Host': 'api.hitem3d.ai',
           "Connection": "keep-alive",
            "Accept-Language": bpy.app.translations.locale.replace("_", "-") if hasattr(bpy.app, "translations") else "en-US"
        }
        response = requests.request("POST", url, headers=headers, data={})
        body = response.json()
        if body.get("code") != 200:
            return None
        data = body.get("data", {}) if isinstance(body, dict) else {}
        token_type = data.get("tokenType") or "Bearer"
        access_token = data.get("accessToken") or ""
        if not access_token:
            return None
        new_auth = f"{token_type} {access_token}"
        def _apply():
            scn = bpy.data.scenes.get(scene_name)
            if scn is not None:
                scn.hi3d_authorization = new_auth
                scn.hi3d_confirmed = True
        _post_to_main_thread(_apply)
        return new_auth
    except Exception:
        return None


def resume_pending_tasks():
    started_gen = 0
    started_tex = 0
    restarted_tex_submit = 0
    for scn in getattr(bpy.data, "scenes", []):
        authorization = getattr(scn, "hi3d_authorization", "")
        if not authorization:
            continue
        gen_tasks = getattr(scn, "hi3d_tasks", [])
        for t in gen_tasks:
            api_task_id = getattr(t, "api_task_id", "")
            if not api_task_id:
                continue
            if getattr(t, "gen_status", "") in {"INIT", "QUEUE", "GEN_GEOM"}:
                if _start_generation_poller(scn.name, t.task_id, api_task_id, authorization):
                    started_gen += 1
        tex_tasks = getattr(scn, "hi3d_texture_tasks", [])
        for t in tex_tasks:
            api_task_id = getattr(t, "api_task_id", "")
            st = getattr(t, "tex_status", "")
            if api_task_id:
                if st in {"INIT", "QUEUE", "GEN_TEX"}:
                    if _start_texture_poller(scn.name, t.task_id, t.create_time, api_task_id, authorization):
                        started_tex += 1
                continue
            if st != "INIT":
                continue
            if _start_texture_submitter(scn.name, t.task_id, t.create_time, authorization):
                restarted_tex_submit += 1
    return started_gen, started_tex, restarted_tex_submit

def _start_generation_poller(scene_name: str, plug_task_id: str, api_task_id: str, authorization: str):
    key = ("GEN", scene_name, plug_task_id)
    with _ACTIVE_POLLERS_LOCK:
        if key in _ACTIVE_POLLERS:
            return False
        _ACTIVE_POLLERS.add(key)
    thread = threading.Thread(target=_poll_generation_existing, args=(scene_name, plug_task_id, api_task_id, authorization, key), daemon=True)
    thread.start()
    return True

def _start_texture_poller(scene_name: str, plug_task_id: str, create_time: str, api_task_id: str, authorization: str):
    key = ("TEX", scene_name, plug_task_id, create_time)
    with _ACTIVE_POLLERS_LOCK:
        if key in _ACTIVE_POLLERS:
            return False
        _ACTIVE_POLLERS.add(key)
    thread = threading.Thread(target=_poll_texture_existing, args=(scene_name, plug_task_id, create_time, api_task_id, authorization, key), daemon=True)
    thread.start()
    return True

def _start_texture_submitter(scene_name: str, plug_task_id: str, create_time: str, authorization: str):
    key = ("TEX_SUBMIT", scene_name, plug_task_id, create_time)
    with _ACTIVE_POLLERS_LOCK:
        if key in _ACTIVE_POLLERS:
            return False
        _ACTIVE_POLLERS.add(key)

    def _work():
        try:
            scene = bpy.data.scenes.get(scene_name)
            tasks = getattr(scene, "hi3d_texture_tasks", None) if scene is not None else None
            target = None
            if tasks is not None:
                for i in range(len(tasks)):
                    if tasks[i].task_id == plug_task_id and tasks[i].create_time == create_time:
                        target = tasks[i]
                        break
            if target is None:
                try:
                    print(f"Hi3D resume: texture task not found: {plug_task_id} {create_time}")
                except Exception:
                    pass
                return
            obj = getattr(target, "obj", None)
            image_path = getattr(target, "image_path", "") or ""
            if obj is None or getattr(obj, "data", None) is None:
                _schedule_update_tex_status(scene_name, plug_task_id, create_time, "FAIL")
                _post_to_main_thread(lambda: _show_error_dialog("Resume texture task failed: target object missing."))
                try:
                    print(f"Hi3D resume: texture task failed (object missing): {plug_task_id} {create_time}")
                except Exception:
                    pass
                return
            if not image_path or not os.path.exists(image_path):
                _schedule_update_tex_status(scene_name, plug_task_id, create_time, "FAIL")
                _post_to_main_thread(lambda: _show_error_dialog("Resume texture task failed: texture image file missing."))
                try:
                    print(f"Hi3D resume: texture task failed (image missing): {plug_task_id} {create_time}")
                except Exception:
                    pass
                return

            face_count = len(getattr(obj.data, "polygons", [])) if getattr(obj, "data", None) else 0
            if face_count < 100000 or face_count > 2000000:
                _schedule_update_tex_status(scene_name, plug_task_id, create_time, "FAIL")
                _post_to_main_thread(lambda: _show_error_dialog("Resume texture task failed: faces of model must be in 10w and 200w."))
                try:
                    print(f"Hi3D resume: texture task failed (face out of range): {plug_task_id} {create_time} faces={face_count}")
                except Exception:
                    pass
                return

            tmp_path = None
            try:
                try:
                    if bpy.context.mode != 'OBJECT':
                        bpy.ops.object.mode_set(mode='OBJECT')
                except Exception:
                    pass
                try:
                    bpy.ops.object.select_all(action='DESELECT')
                except Exception:
                    pass
                try:
                    obj.select_set(True)
                    bpy.context.view_layer.objects.active = obj
                except Exception:
                    pass
                ext = ".glb"
                tmpf = tempfile.NamedTemporaryFile(delete=False, suffix=ext)
                tmp_path = tmpf.name
                tmpf.close()
                bpy.ops.export_scene.gltf(
                    filepath=tmp_path,
                    export_format='GLB',
                    use_selection=True,
                    export_apply=True,
                    export_yup=False,
                )
                try:
                    print(f"Hi3D resume: re-submitting texture task {plug_task_id} {create_time}")
                except Exception:
                    pass
                thread = threading.Thread(
                    target=texture_worker,
                    args=(scene_name, authorization, tmp_path, image_path, plug_task_id, create_time),
                    daemon=True,
                )
                thread.start()
            except Exception as e:
                _schedule_update_tex_status(scene_name, plug_task_id, create_time, "FAIL")
                _post_to_main_thread(lambda m=str(e): _show_error_dialog(f"Resume texture task failed: {m}"))
                try:
                    print(f"Hi3D resume: texture task failed (exception): {plug_task_id} {create_time} err={e}")
                except Exception:
                    pass
                if tmp_path and os.path.exists(tmp_path):
                    try:
                        os.remove(tmp_path)
                    except Exception:
                        pass
        finally:
            with _ACTIVE_POLLERS_LOCK:
                _ACTIVE_POLLERS.discard(key)

    try:
        bpy.app.timers.register(_work, first_interval=0.5)
    except Exception:
        threading.Thread(target=_work, daemon=True).start()
    return True

def _poll_generation_existing(scene_name: str, plug_task_id: str, api_task_id: str, authorization: str, key):
    try:
        status_url = "https://api.hitem3d.ai/open-api/v1/query-task?task_id=" + api_task_id
        set_started = False
        set_queue = False
        set_done = False
        set_failed = False
        def _get_headers():
            scn = bpy.data.scenes.get(scene_name)
            auth = getattr(scn, "hi3d_authorization", "") if scn is not None else authorization
            if not auth:
                raise RuntimeError("No authorization")
            return _build_headers(auth)
        def _handle(state, data_obj):
            nonlocal set_queue, set_started, set_done, set_failed
            if state == "created":
                return True
            elif state == "queueing":
                if not set_queue:
                    _schedule_update_gen_status(scene_name, plug_task_id, "QUEUE")
                    set_queue = True
                return True
            elif state == "processing":
                if not set_started:
                    _schedule_update_gen_status(scene_name, plug_task_id, "GEN_GEOM")
                    set_started = True
                return True
            elif state == "success":
                st = data_obj.get("url")
                if not st:
                    if not set_failed:
                        _schedule_update_gen_status(scene_name, plug_task_id, "FAIL")
                        set_failed = True
                    return False
                ext = ".fbx"
                tmpf = tempfile.NamedTemporaryFile(delete=False, suffix=ext)
                tmp_path = tmpf.name
                tmpf.close()
                r = requests.get(st, headers={"User-Agent": "Apifox/1.0.0 (https://apifox.com)"}, stream=True, timeout=_REQ_TIMEOUT)
                if r.status_code != 200:
                    if not set_failed:
                        _schedule_update_gen_status(scene_name, plug_task_id, "FAIL")
                        set_failed = True
                    return False
                with open(tmp_path, "wb") as out:
                    for chunk in r.iter_content(chunk_size=8192):
                        if chunk:
                            out.write(chunk)
                def import_and_cleanup(_tmp_path=tmp_path, _scene_name=scene_name, _plug_task_id=plug_task_id):
                    try:
                        scene = bpy.data.scenes.get(_scene_name)
                        tasks = getattr(scene, "hi3d_tasks", None) if scene is not None else None
                        def _set_fail():
                            if tasks is not None:
                                for i in range(len(tasks)):
                                    if tasks[i].task_id == _plug_task_id:
                                        tasks[i].gen_status = "FAIL"
                                        break
                        existing_objects = set(bpy.data.objects[:])
                        if bpy.context.mode != 'OBJECT':
                            bpy.ops.object.mode_set(mode='OBJECT')
                        bpy.ops.object.select_all(action='DESELECT')
                        if hasattr(bpy.ops.import_scene, "fbx"):
                            try:
                                bpy.ops.import_scene.fbx('EXEC_DEFAULT', filepath=_tmp_path)
                            except Exception:
                                bpy.ops.import_scene.fbx(filepath=_tmp_path)
                        else:
                            _show_error_dialog("FBX importer not available. Enable 'Import-Export: FBX format' add-on.")
                            _set_fail()
                            return None
 
                        new_objects = list(set(bpy.data.objects[:]) - existing_objects)
                        if new_objects:
                            active_col = bpy.context.view_layer.active_layer_collection.collection
                            for obj2 in new_objects:
                                obj2.select_set(True)
                                if active_col not in obj2.users_collection:
                                    active_col.objects.link(obj2)
                                for col in list(obj2.users_collection):
                                    if col != active_col:
                                        col.objects.unlink(obj2)
                                obj2.name = f"hi3d_model_{_plug_task_id}"
                            bpy.context.view_layer.objects.active = new_objects[0]
                            if tasks is not None:
                                for i in range(len(tasks)):
                                    if tasks[i].task_id == _plug_task_id:
                                        tasks[i].obj = new_objects[0]
                                        break
                    except Exception as e:
                        _show_error_dialog(f"Import model failed: {e}")
                        try:
                            _set_fail()
                        except Exception:
                            pass
                    finally:
                        if os.path.exists(_tmp_path):
                            os.remove(_tmp_path)
                    return None
                _post_to_main_thread(import_and_cleanup)
                if not set_done:
                    _schedule_update_gen_status(scene_name, plug_task_id, "READY_TEX")
                    set_done = True
                return False
            elif state in {"failed", "failure", "error", "cancelled", "canceled", "timeout", "expired"}:
                if not set_failed:
                    _schedule_update_gen_status(scene_name, plug_task_id, "FAIL")
                    set_failed = True
                return False
            else:
                if not set_failed:
                    _schedule_update_gen_status(scene_name, plug_task_id, "FAIL")
                    set_failed = True
                return False
        res = _poll_status(status_url, _get_headers, _handle)
        if not set_done and not set_failed and not res.get("ok"):
            _schedule_update_gen_status(scene_name, plug_task_id, "FAIL")
    finally:
        with _ACTIVE_POLLERS_LOCK:
            _ACTIVE_POLLERS.discard(key)

def _poll_texture_existing(scene_name: str, plug_task_id: str, create_time: str, api_task_id: str, authorization: str, key):
    try:
        status_url = "https://api.hitem3d.ai/open-api/v1/query-task?task_id=" + api_task_id
        set_started = False
        set_queue = False
        set_done = False
        set_failed = False
        def _get_headers():
            scn = bpy.data.scenes.get(scene_name)
            auth = getattr(scn, "hi3d_authorization", "") if scn is not None else authorization
            if not auth:
                raise RuntimeError("No authorization")
            return _build_headers(auth)
        def _handle(state, data_obj):
            nonlocal set_queue, set_started, set_done, set_failed
            if state == "created":
                return True
            elif state == "queueing":
                if not set_queue:
                    _schedule_update_tex_status(scene_name, plug_task_id, create_time, "QUEUE")
                    set_queue = True
                return True
            elif state == "processing":
                if not set_started:
                    _schedule_update_tex_status(scene_name, plug_task_id, create_time, "GEN_TEX")
                    set_started = True
                return True
            elif state == "success":
                st = data_obj.get("url")
                if not st:
                    if not set_failed:
                        _schedule_update_tex_status(scene_name, plug_task_id, create_time, "FAIL")
                        set_failed = True
                    return False
                ext = ".fbx"
                tmpf = tempfile.NamedTemporaryFile(delete=False, suffix=ext)
                tmp_path = tmpf.name
                tmpf.close()
                r = requests.get(st, headers={"User-Agent": "Apifox/1.0.0 (https://apifox.com)"}, stream=True, timeout=_REQ_TIMEOUT)
                if r.status_code != 200:
                    if not set_failed:
                        _schedule_update_tex_status(scene_name, plug_task_id, create_time, "FAIL")
                        set_failed = True
                    return False
                with open(tmp_path, "wb") as out:
                    for chunk in r.iter_content(chunk_size=8192):
                        if chunk:
                            out.write(chunk)
                def import_and_cleanup_tex(_tmp_path=tmp_path, _scene_name=scene_name, _plug_task_id=plug_task_id, _create_time=create_time):
                    try:
                        scene = bpy.data.scenes.get(_scene_name)
                        tasks = getattr(scene, "hi3d_texture_tasks", None) if scene is not None else None
                        def _set_fail():
                            if tasks is not None:
                                for i in range(len(tasks)):
                                    if tasks[i].task_id == _plug_task_id and tasks[i].create_time == _create_time:
                                        tasks[i].tex_status = "FAIL"
                                        break
                        existing_objects = set(bpy.data.objects[:])
                        if bpy.context.mode != 'OBJECT':
                            bpy.ops.object.mode_set(mode='OBJECT')
                        bpy.ops.object.select_all(action='DESELECT')
                        if hasattr(bpy.ops.import_scene, "fbx"):
                            try:
                                bpy.ops.import_scene.fbx('EXEC_DEFAULT', filepath=_tmp_path)
                            except Exception:
                                bpy.ops.import_scene.fbx(filepath=_tmp_path)
                        else:
                            _show_error_dialog("FBX importer not available. Enable 'Import-Export: FBX format' add-on.")
                            _set_fail()
                            return None
 
                        new_objects = list(set(bpy.data.objects[:]) - existing_objects)
                        if new_objects:
                            active_col = bpy.context.view_layer.active_layer_collection.collection
                            for obj2 in new_objects:
                                obj2.select_set(True)
                                if active_col not in obj2.users_collection:
                                    active_col.objects.link(obj2)
                                for col in list(obj2.users_collection):
                                    if col != active_col:
                                        col.objects.unlink(obj2)
                                obj2.name = f"hi3d_model_{_plug_task_id}"
                            bpy.context.view_layer.objects.active = new_objects[0]
                            if tasks is not None:
                                for i in range(len(tasks)):
                                    if tasks[i].task_id == _plug_task_id and tasks[i].create_time == _create_time:
                                        tasks[i].obj = new_objects[0]
                                        break
                    except Exception as e:
                        _show_error_dialog(f"Import texture model failed: {e}")
                        try:
                            _set_fail()
                        except Exception:
                            pass
                    finally:
                        if os.path.exists(_tmp_path):
                            os.remove(_tmp_path)
                    return None
                _post_to_main_thread(import_and_cleanup_tex)
                if not set_done:
                    _schedule_update_tex_status(scene_name, plug_task_id, create_time, "DONE")
                    set_done = True
                return False
            elif state in {"failed", "failure", "error", "cancelled", "canceled", "timeout", "expired"}:
                if not set_failed:
                    _schedule_update_tex_status(scene_name, plug_task_id, create_time, "FAIL")
                    set_failed = True
                return False
            else:
                if not set_failed:
                    _schedule_update_tex_status(scene_name, plug_task_id, create_time, "FAIL")
                    set_failed = True
                return False
        res = _poll_status(status_url, _get_headers, _handle)
        if not set_done and not set_failed and not res.get("ok"):
            _schedule_update_tex_status(scene_name, plug_task_id, create_time, "FAIL")
    finally:
        with _ACTIVE_POLLERS_LOCK:
            _ACTIVE_POLLERS.discard(key)
