import bpy
from bpy.types import Operator
from bpy.app.handlers import persistent
from .utils import open_url, generation_worker, texture_worker, refresh_authorization_from_scene
import os
import uuid
import threading
from datetime import datetime
import base64
import requests
import re
import tempfile


# Validate image files
valid_exts = {'.png', '.jpg', '.jpeg', '.bmp', '.tga', '.tiff', '.webp'}
    
def _check_img_file(path, name):
    if not path or not os.path.exists(path):
        return f"{name} file not found: {path}"
    if os.path.splitext(path)[1].lower() not in valid_exts:
        return f"{name} must be an image file (PNG, JPG, JPEG, BMP, TGA, TIFF, WEBP)"
    return None


class HI3D_OT_open_website(Operator):
    bl_idname = "hi3d.open_website"
    bl_label = "Open Hi3D"
    bl_options = {'REGISTER'}

    def execute(self, context):
        url = getattr(context.scene, "hi3d_url", "") or "https://www.hitem3d.ai/"
        open_url(url)
        return {'FINISHED'}
class HI3D_OT_view_tutorial(Operator):
    bl_idname = "hi3d.view_tutorial"
    bl_label = "View Tutorial"
    bl_description = "Open the official Hi3D tutorial in your web browser"
    bl_options = {'REGISTER'}

    def execute(self, context):
        open_url("https://www.hitem3d.ai/blog/How-to-Generate-AI-3D-Models-in-Blender-Using-the-Hitem3D-Plugin-Step-by-Step-Guide-Hitem3D/#Step-1-Authenticate-the-Plugin")
        return {'FINISHED'}
    
class HI3D_OT_open_billing(Operator):
    bl_idname = "hi3d.open_billing"
    bl_label = "Go Billing"
    bl_options = {'REGISTER'}

    def execute(self, context):
        open_url("https://platform.hitem3d.ai/console/apiKey")
        return {'FINISHED'}

class HI3D_OT_confirm_api_key(Operator):
    bl_idname = "hi3d.confirm_api_key"
    bl_label = "Confirm"
    bl_options = {'REGISTER'}

    def execute(self, context):
        scn = context.scene
        ak = getattr(scn, "hi3d_access_key", "") or ""
        sk = getattr(scn, "hi3d_secret_key", "") or ""
        if not ak or not sk:
            self.report({'ERROR'}, "Please enter both Access Key and Secret Key")
            return {'CANCELLED'}
        if not ak.startswith("ak_"):
            self.report({'ERROR'}, "Access Key must start with 'ak_'")
            return {'CANCELLED'}
        if not sk.startswith("sk_"):
            self.report({'ERROR'}, "Secret Key must start with 'sk_'")
            return {'CANCELLED'}
        token = f"{ak}:{sk}".encode("utf-8")
        auth = "Basic " + base64.b64encode(token).decode("ascii")

        url = "https://api.hitem3d.ai/open-api/v1/auth/token"
        payload={}
        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("_", "-")
        }
        response = requests.request("POST", url, headers=headers, data=payload)

        try:
            body = response.json()
        except Exception:
            self.report({'ERROR'}, "Failed to parse response")
            return {'CANCELLED'}
        if body.get("code") != 200:
            msg = body.get("msg") or "Unknown error"
            self.report({'ERROR'}, f"Failed to confirm API keys: {msg}")
            return {'CANCELLED'}

        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:
            self.report({'ERROR'}, "Missing accessToken in response")
            return {'CANCELLED'}
        scn.hi3d_authorization = f"{token_type} {access_token}"
        scn.hi3d_confirmed = True
        _write_api_keys_to_text(ak, sk, auth)
        self.report({'INFO'}, "API keys confirmed")
        try:
            print("Hi3D confirm: authorization updated")
        except Exception:
            pass

        Balance_url = "https://api.hitem3d.ai/open-api/v1/auth/balance"
        Balance_headers = {
            "Authorization": scn.hi3d_authorization,
            "User-Agent": "Apifox/1.0.0 (https://apifox.com)",
            "Accept": "*/*",
            "Host": "api.hitem3d.ai",
            "Connection": "keep-alive"
        }
        response = requests.request("GET", Balance_url, headers=Balance_headers)
        try:
            body = response.json()
        except Exception:
            self.report({'ERROR'}, "Failed to parse response")
            return {'CANCELLED'}
        if body.get("code") != 200:
            msg = body.get("msg") or "Unknown error"
            self.report({'ERROR'}, f"Failed to get balance: {msg}")
            return {'CANCELLED'}
        data = body.get("data", {}) if isinstance(body, dict) else {}
        balance = data.get("totalBalance") or 0
        scn.hi3d_balance = float(balance)
        try:
            print(f"Hi3D confirm: balance={float(balance)}")
        except Exception:
            pass

        return {'FINISHED'}

# New preferred file name (Hi3D). Legacy name kept for backward compatibility
# so users who had keys saved by older versions do not lose them.
_API_KEYS_FILENAME = "Hi3D_API_Keys.txt"
_API_KEYS_FILENAME_LEGACY = "Hitem3D_API_Keys.txt"


def _resolve_api_keys_path_for_read():
    addon_dir = os.path.dirname(os.path.realpath(__file__))
    new_path = os.path.join(addon_dir, _API_KEYS_FILENAME)
    if os.path.exists(new_path):
        return new_path
    legacy_path = os.path.join(addon_dir, _API_KEYS_FILENAME_LEGACY)
    if os.path.exists(legacy_path):
        return legacy_path
    return None


def _write_api_keys_to_text(ak: str, sk: str, auth: str):
    addon_dir = os.path.dirname(os.path.realpath(__file__))
    key_file = os.path.join(addon_dir, _API_KEYS_FILENAME)
    with open(key_file, "wb") as f:
        f.write(f"Access Key: {ak}\nSecret Key: {sk}\nAuthorization: {auth}".encode("utf-8"))

def _read_api_keys_from_text_into_scene(*args, **kwargs):
    try:
        key_file = _resolve_api_keys_path_for_read()
        if key_file is None:
            print("Hi3D load_post: key file not found")
            return False
        with open(key_file, "rb") as f:
            lines = f.readlines()
        lines = [l.decode("utf-8").strip() for l in lines]
        ak = ""
        sk = ""
        auth = ""
        for line in lines:
            if line.startswith("Access Key:"):
                ak = line.split(":")[1].strip()
            elif line.startswith("Secret Key:"):
                sk = line.split(":")[1].strip()
            elif line.startswith("Authorization:"):
                auth = line.split(":")[1].strip()
        try:
            scn = bpy.context.scene
        except Exception:
            scn = None
        if scn:
            if ak:
                scn.hi3d_access_key = ak
            if sk:
                scn.hi3d_secret_key = sk
            if auth:
                scn.hi3d_authorization = auth
            print("Hi3D load_post: api keys loaded")
            return True
        print("Hi3D load_post: no active scene to load keys")
        return False
    except Exception as e:
        print(f"Hi3D load_post: load keys failed: {e}")
        return False

@persistent
def _refresh_authorization_on_load(*args, **kwargs):
    def _do():
        try:
            scn = bpy.context.scene
        except Exception:
            scn = None
        if not scn:
            print("Hi3D load_post: no active scene for auth refresh")
            return None
        try:
            print("Hi3D load_post: refreshing authorization...")
            new_auth = refresh_authorization_from_scene(scn.name)
        except Exception as e:
            print(f"Hi3D load_post: refresh authorization failed: {e}")
            return None
        if not new_auth:
            print("Hi3D load_post: authorization refresh returned empty")
            return None
        try:
            print("Hi3D load_post: authorization refreshed")
            Balance_url = "https://api.hitem3d.ai/open-api/v1/auth/balance"
            Balance_headers = {
                "Authorization": new_auth,
                "User-Agent": "Apifox/1.0.0 (https://apifox.com)",
                "Accept": "*/*",
                "Host": "api.hitem3d.ai",
                "Connection": "keep-alive"
            }
            response = requests.request("GET", Balance_url, headers=Balance_headers)
            body = response.json()
            if isinstance(body, dict) and body.get("code") == 200:
                data = body.get("data", {}) if isinstance(body, dict) else {}
                balance = data.get("totalBalance") or 0
                scn.hi3d_balance = float(balance)
                print(f"Hi3D load_post: balance={float(balance)}")
            else:
                msg = (body.get("msg") if isinstance(body, dict) else None) or "Unknown error"
                print(f"Hi3D load_post: balance fetch failed: {msg}")
        except Exception as e:
            print(f"Hi3D load_post: balance fetch error: {e}")
        return None

    try:
        bpy.app.timers.register(_do, first_interval=1.0)
    except Exception as e:
        print(f"Hi3D load_post: schedule auth refresh failed: {e}")

class HI3D_OT_set_task(Operator):
    bl_idname = "hi3d.set_task"
    bl_label = "Set Task"
    bl_options = {'INTERNAL'}
    task: bpy.props.EnumProperty(
        items=(
            ("IMAGE", "Image", ""),
            ("MULTI", "Multi", ""),
        ),
        name="Task",
    )

    def execute(self, context):
        context.scene.hi3d_task_type = self.task
        return {'FINISHED'}

class HI3D_OT_set_model_type(Operator):
    bl_idname = "hi3d.set_model_type"
    bl_label = "Set Model Type"
    bl_options = {'INTERNAL'}
    type: bpy.props.EnumProperty(
        items=(
            ("GENERAL", "General", ""),
            ("PORTRAIT", "Portrait", ""),
        ),
        name="Type",
    )

    def execute(self, context):
        context.scene.hi3d_model_type = self.type
        return {'FINISHED'}

class HI3D_OT_select_image(Operator):
    bl_idname = "hi3d.select_image"
    bl_label = "Upload Image"
    bl_options = {'REGISTER'}
    filepath: bpy.props.StringProperty(subtype='FILE_PATH')

    def execute(self, context):
        path = self.filepath
        if not path or not os.path.exists(path):
            self.report({'WARNING'}, "Invalid file")
            return {'CANCELLED'}
            
        # Validate image file
        err = _check_img_file(path, "Input Image")
        if err:
             self.report({'ERROR'}, err)
             return {'CANCELLED'}
             
        img = bpy.data.images.load(path)
        context.scene.hi3d_input_image = img
        return {'FINISHED'}

    def invoke(self, context, event):
        context.window_manager.fileselect_add(self)
        return {'RUNNING_MODAL'}


class HI3D_OT_generate_model(Operator):
    bl_idname = "hi3d.generate_model"
    bl_label = "Generate"
    bl_options = {'REGISTER'}

    def execute(self, context):
        ttype = "image_to_model" if getattr(context.scene, "hi3d_task_type", "IMAGE") == "IMAGE" else "multi_to_model"
        uid = str(uuid.uuid4())

        input_image = getattr(context.scene, "hi3d_input_image", None)
        front_image = getattr(context.scene, "front_image", None)
        if not input_image and not front_image:
            self.report({'ERROR'}, "No input image or front image selected. Please upload an image first.")
            return {'CANCELLED'}

        if ttype == "image_to_model":
            path = getattr(context.scene, "hi3d_input_image_path", "")
            # If image object exists but path is empty, it's an error for our file-based API
            if getattr(context.scene, "hi3d_input_image", None):
                if not path:
                    self.report({'ERROR'}, "Input image path is missing. Please reload the image.")
                    return {'CANCELLED'}
                err = _check_img_file(path, "Input Image")
                if err:
                    self.report({'ERROR'}, err)
                    return {'CANCELLED'}
        else:
            # Check images
            for name, prop_path in [("Front Image", "front_image_path"), ("Back Image", "back_image_path"), ("Left Image", "left_image_path"), ("Right Image", "right_image_path")]:
                p = getattr(context.scene, prop_path, "")
                if p:
                    err = _check_img_file(p, name)
                    if err:
                        self.report({'ERROR'}, err)
                        return {'CANCELLED'}

        item = context.scene.hi3d_tasks.add()
        item.task_id = uid
        item.task_type = ttype
        item.create_time = datetime.now().strftime("%Y/%m/%d %H:%M:%S")

        if ttype == "image_to_model":
            item.image = getattr(context.scene, "hi3d_input_image", None)
            item.image_path = getattr(context.scene, "hi3d_input_image_path", "")
        else:
            item.image = getattr(context.scene, "front_image", None)
            item.image_path = getattr(context.scene, "front_image_path", "")
        item.gen_status = "INIT"

        try:
            context.scene.hi3d_is_generating = True
            scn = context.scene
            thread = threading.Thread(
                target=generation_worker,
                args=(
                    scn.name,
                    getattr(scn, "hi3d_authorization", ""),
                    ttype,
                    uid,
                    getattr(scn, "hi3d_resolution", "1536"),
                    getattr(scn, "hi3d_face_limit", 2000000),
                    getattr(scn, "hi3d_model_type", "GENERAL"),
                    getattr(scn, "hi3d_model_version", "v2_0"),
                    getattr(scn, "hi3d_input_image_path", ""),
                    getattr(scn, "front_image_path", ""),
                    getattr(scn, "back_image_path", ""),
                    getattr(scn, "left_image_path", ""),
                    getattr(scn, "right_image_path", ""),
                ),
                daemon=True,
            )
            thread.start()

            def reset_generating_flag():
                context.scene.hi3d_is_generating = False
                return None

            bpy.app.timers.register(reset_generating_flag, first_interval=3.0)
            return {'FINISHED'}
        except Exception as e:
            item.gen_status = "FAIL"
            context.scene.hi3d_is_generating = False
            self.report({'ERROR'}, f"Generate failed: {e}")
            return {'CANCELLED'}

class LoadBaseImageOperator(Operator):
    bl_idname = "hi3d.load_base_image"
    bl_label = "Load Image from Path"
    target_prop = None

    filepath: bpy.props.StringProperty(subtype="FILE_PATH")

    def execute(self, context):
        if not self.filepath:
            self.report({"ERROR"}, "No file selected")
            return {"CANCELLED"}

        try:
            image = bpy.data.images.load(self.filepath)
            name = getattr(self, "target_prop", None)
            if not name:
                id_name = getattr(self, "bl_idname", self.__class__.__name__)
                suffix = id_name.split(".")[-1].lower()
                mapping = {
                    "load_image": "hi3d_input_image",
                    "load_texture_image": "hi3d_texture_image",
                    "load_left_image": "left_image",
                    "load_right_image": "right_image",
                    "load_front_image": "front_image",
                    "load_back_image": "back_image",
                }
                name = mapping.get(suffix, None)
            if not name:
                self.report({"ERROR"}, f"Unsupported operator: {self.__class__.__name__}")
                return {"CANCELLED"}
            setattr(context.scene, name, image)
            mapping_path = {
                "hi3d_input_image": "hi3d_input_image_path",
                "hi3d_texture_image": "hi3d_texture_image_path",
                "left_image": "left_image_path",
                "right_image": "right_image_path",
                "front_image": "front_image_path",
                "back_image": "back_image_path",
            }
            path_prop = mapping_path.get(name, None)
            if path_prop:
                setattr(context.scene, path_prop, self.filepath)
            self.report({"INFO"}, f"Image loaded: {self.filepath}")
            return {"FINISHED"}

        except Exception as e:
            self.report({"ERROR"}, f"Failed to load image: {str(e)}")
            return {"CANCELLED"}

    def invoke(self, context, event):
        context.window_manager.fileselect_add(self)
        return {"RUNNING_MODAL"}

class LoadImageOperator(LoadBaseImageOperator):
    bl_idname = "hi3d.load_image"
    target_prop = "hi3d_input_image"

class LoadTextureImageOperator(LoadBaseImageOperator):
    bl_idname = "hi3d.load_texture_image"
    target_prop = "hi3d_texture_image"

class LoadLeftImageOperator(LoadBaseImageOperator):
    bl_idname = "hi3d.load_left_image"
    target_prop = "left_image"

class LoadRightImageOperator(LoadBaseImageOperator):
    bl_idname = "hi3d.load_right_image"
    target_prop = "right_image"

class LoadFrontImageOperator(LoadBaseImageOperator):
    bl_idname = "hi3d.load_front_image"
    target_prop = "front_image"

class LoadBackImageOperator(LoadBaseImageOperator):
    bl_idname = "hi3d.load_back_image"
    target_prop = "back_image"
    
class HI3D_OT_add_texture_task(Operator):
    bl_idname = "hi3d.add_texture_task"
    bl_label = "Add to Texture Task"
    bl_options = {'REGISTER'}

    def execute(self, context):
        scene = context.scene
        obj = context.selected_objects[0] if context.selected_objects else None
        if obj is None:
            scene.hi3d_last_added_object_name = ""
            scene.hi3d_selected_texture_task_id = ""
            scene.hi3d_selected_texture_task_type = ""
            scene.hi3d_selected_texture_task_create_time = ""
            self.report({'WARNING'}, "Texture Generation · Selection. Select one object to get texture...")
            return {'CANCELLED'}
        scene.hi3d_last_added_object_name = obj.name
        gen_tasks = scene.hi3d_tasks
        tex_tasks = scene.hi3d_texture_tasks
        found_idx = None
        for i in range(len(gen_tasks)):
            if gen_tasks[i].obj == obj:
                found_idx = i
                break
        if found_idx is not None:
            coll = scene.hi3d_selected_texture_task
            for j in reversed(range(len(coll))):
                coll.remove(j)
            item = coll.add()
            item.index = found_idx
            item.type = "GEN"
            self.report({'INFO'}, f"Selected existing generate task for {obj.name}.")
        else:
            for i in range(len(tex_tasks)):
                if tex_tasks[i].obj == obj:
                    found_idx = i
                    break
            if found_idx is not None:
                coll = scene.hi3d_selected_texture_task
                for j in reversed(range(len(coll))):
                    coll.remove(j)
                item = coll.add()
                item.index = found_idx
                item.type = "TEX"
                scene.hi3d_selected_texture_task_index = found_idx
                self.report({'INFO'}, f"Selected existing texture task for {obj.name}.")
            else:
                coll = scene.hi3d_selected_texture_task
                for j in reversed(range(len(coll))):
                    coll.remove(j)
                self.report({'INFO'}, f"Texture Generation · Selection. This object is not managed by Hi3D yet. It will be added to Hi3D.")
        
        selected_texture_task_coll = getattr(scene, "hi3d_selected_texture_task", [])
        selected_texture_task = selected_texture_task_coll[0] if len(selected_texture_task_coll) > 0 else None
        target = None
        if selected_texture_task is not None:
            if selected_texture_task.type == "TEX":
                if 0 <= selected_texture_task.index < len(tex_tasks):
                    target = tex_tasks[selected_texture_task.index]
            elif selected_texture_task.type == "GEN":
                if 0 <= selected_texture_task.index < len(gen_tasks):
                    target = gen_tasks[selected_texture_task.index]
        if target is not None:
            scene.hi3d_selected_texture_task_id = target.task_id
            scene.hi3d_selected_texture_task_type = target.task_type
            scene.hi3d_selected_texture_task_create_time = target.create_time
            if selected_texture_task.type == "GEN":
                scene.hi3d_texture_image = target.image
                scene.hi3d_texture_image_path = target.image_path
            else:
                scene.hi3d_texture_image = None
                scene.hi3d_texture_image_path = ""
        else:
            scene.hi3d_selected_texture_task_id = ""
            scene.hi3d_selected_texture_task_type = ""
            scene.hi3d_selected_texture_task_create_time = ""
            scene.hi3d_texture_image = None
            scene.hi3d_texture_image_path = ""
        
        return {'FINISHED'}

class HI3D_OT_generate_texture(Operator):
    bl_idname = "hi3d.generate_texture"
    bl_label = "Generate Texture"
    bl_options = {'REGISTER'}

    def execute(self, context):
        scn = context.scene
        last_name = getattr(scn, "hi3d_last_added_object_name", "")
        if last_name:
            obj = bpy.data.objects.get(last_name)
        else:
            obj = None
        if obj is None:
            self.report({'WARNING'}, "No object has been added.")
            return {'CANCELLED'}
        task_id = getattr(scn, "hi3d_selected_texture_task_id", "")
        if task_id == "":
            task_id = str(uuid.uuid4())

        if scn.hi3d_texture_image is None:
            self.report({'WARNING'}, "No texture image has been uploaded.")
            return {'CANCELLED'}
        
        # Validate texture image file
        path = getattr(scn, "hi3d_texture_image_path", "")
        if not path:
             self.report({'ERROR'}, "Texture image path is missing. Please reload the image.")
             return {'CANCELLED'}
        err = _check_img_file(path, "Texture Image")
        if err:
             self.report({'ERROR'}, err)
             return {'CANCELLED'}
             
        face_count = len(getattr(obj.data, "polygons", [])) if getattr(obj, "data", None) else 0
        if face_count < 100000 or face_count > 2000000:
            self.report({'ERROR'}, "Faces of model must be in 10w and 200w.")
            return {'CANCELLED'}
        
        tasks = scn.hi3d_texture_tasks

        target = tasks.add()
        target.task_id = task_id
        target.task_type = "texture_generation"
        target.create_time = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
        target.obj = obj
        target.image = scn.hi3d_texture_image
        target.image_path = scn.hi3d_texture_image_path

        # target = None
        # for t in tasks:
        #     if t.obj == obj:
        #         target = t
        #         break
        # if target is None:
        #     target = tasks.add()
        #     target.task_id = task_id
        #     target.task_type = "texture_generation"
        #     target.create_time = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
        #     target.obj = obj
        #     target.image = scn.hi3d_texture_image
        #     target.image_path = scn.hi3d_texture_image_path
        # else:
        #     target.create_time = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
        #     target.obj = obj
        #     target.image = scn.hi3d_texture_image
        #     target.image_path = scn.hi3d_texture_image_path
        target.tex_status = "INIT"
        self.report({'INFO'}, f"Generating texture for {obj.name}.")

        tmp_path = None
        try:
            ext = ".glb"
            tmpf = tempfile.NamedTemporaryFile(delete=False, suffix=ext)
            tmp_path = tmpf.name
            tmpf.close()

            bpy.ops.object.select_all(action='DESELECT')
            obj.select_set(True)
            bpy.ops.export_scene.gltf(
                filepath=tmp_path,
                export_format='GLB',
                use_selection=True,
                export_apply=True,
                export_yup=False,
            )

            scn.hi3d_is_generating_texture = True
            auth = getattr(scn, "hi3d_authorization", "")

            thread = threading.Thread(
                target=texture_worker,
                args=(scn.name, auth, tmp_path, scn.hi3d_texture_image_path, target.task_id, target.create_time),
                daemon=True,
            )
            thread.start()

            def reset_generating_texture_flag():
                scn.hi3d_is_generating_texture = False
                return None

            bpy.app.timers.register(reset_generating_texture_flag, first_interval=3.0)
            return {'FINISHED'}
        except Exception as e:
            target.tex_status = "FAIL"
            scn.hi3d_is_generating_texture = False
            if tmp_path and os.path.exists(tmp_path):
                try:
                    os.remove(tmp_path)
                except Exception:
                    pass
            self.report({'ERROR'}, f"Texture generation failed: {e}")
            return {'CANCELLED'}

class HI3D_OT_reload_texture_image(Operator):
    bl_idname = "hi3d.reload_texture_image"
    bl_label = "Upload Texture Image"
    bl_options = {'REGISTER'}
    filepath: bpy.props.StringProperty(subtype='FILE_PATH')

    def execute(self, context):
        path = self.filepath
        scene = context.scene
        tasks = getattr(scene, "hi3d_texture_tasks", [])
        idx = getattr(scene, "hi3d_selected_texture_task_index", 0)
        if not tasks or idx < 0 or idx >= len(tasks):
            self.report({'WARNING'}, "No texture task selected")
            return {'CANCELLED'}
        if not path or not os.path.exists(path):
            self.report({'WARNING'}, "Invalid file")
            return {'CANCELLED'}
            
        # Validate texture image file
        err = _check_img_file(path, "Texture Image")
        if err:
             self.report({'ERROR'}, err)
             return {'CANCELLED'}
             
        img = bpy.data.images.load(path)
        tasks[idx].tex_status = "INIT"
        tasks[idx].image = img
        tasks[idx].image_path = path
        scene.hi3d_texture_image_path = path
        auth = getattr(scene, "hi3d_authorization", "")

        tmp_path = None
        try:
            ext = ".glb"
            tmpf = tempfile.NamedTemporaryFile(delete=False, suffix=ext)
            tmp_path = tmpf.name
            tmpf.close()

            bpy.ops.object.select_all(action='DESELECT')
            tasks[idx].obj.select_set(True)
            bpy.ops.export_scene.gltf(
                filepath=tmp_path,
                export_format='GLB',
                use_selection=True,
                export_apply=True,
                export_yup=False,
            )

            scene.hi3d_is_replacing_texture = True

            thread = threading.Thread(
                target=texture_worker,
                args=(scene.name, auth, tmp_path, scene.hi3d_texture_image_path, tasks[idx].task_id, tasks[idx].create_time),
                daemon=True,
            )
            thread.start()

            def reset_replacing_texture_flag():
                scene.hi3d_is_replacing_texture = False
                return None

            bpy.app.timers.register(reset_replacing_texture_flag, first_interval=3.0)
            return {'FINISHED'}
        except Exception as e:
            tasks[idx].tex_status = "FAIL"
            scene.hi3d_is_replacing_texture = False
            if tmp_path and os.path.exists(tmp_path):
                try:
                    os.remove(tmp_path)
                except Exception:
                    pass
            self.report({'ERROR'}, f"Texture generation failed: {e}")
            return {'CANCELLED'}

    def invoke(self, context, event):
        context.window_manager.fileselect_add(self)
        return {'RUNNING_MODAL'}
        
class HI3D_OT_show_error(Operator):
    bl_idname = "hi3d.show_error"
    bl_label = "Hi3D Error"
    bl_options = {'INTERNAL'}

    message: bpy.props.StringProperty(name="Message")

    def execute(self, context):
        return {'FINISHED'}

    def invoke(self, context, event):
        return context.window_manager.invoke_props_dialog(self, width=400)

    def draw(self, context):
        layout = self.layout
        layout.label(text=self.message, icon='ERROR')

classes = (
    HI3D_OT_open_website,
    HI3D_OT_view_tutorial,
    HI3D_OT_open_billing,
    HI3D_OT_confirm_api_key,
    HI3D_OT_set_task,
    HI3D_OT_set_model_type,
    HI3D_OT_select_image,
    HI3D_OT_generate_model,
    HI3D_OT_add_texture_task,
    HI3D_OT_generate_texture,
    HI3D_OT_reload_texture_image,
    HI3D_OT_show_error,
    LoadImageOperator,
    LoadTextureImageOperator,
    LoadLeftImageOperator,
    LoadRightImageOperator,
    LoadFrontImageOperator,
    LoadBackImageOperator,
)


def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    
    if _read_api_keys_from_text_into_scene not in bpy.app.handlers.load_post:
        bpy.app.handlers.load_post.append(_read_api_keys_from_text_into_scene)
    if _refresh_authorization_on_load not in bpy.app.handlers.load_post:
        bpy.app.handlers.load_post.append(_refresh_authorization_on_load)


def unregister():
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)

    if _read_api_keys_from_text_into_scene in bpy.app.handlers.load_post:
        bpy.app.handlers.load_post.remove(_read_api_keys_from_text_into_scene)
    if _refresh_authorization_on_load in bpy.app.handlers.load_post:
        bpy.app.handlers.load_post.remove(_refresh_authorization_on_load)
