import bpy
from bpy.types import Operator
from .utils import open_url, generation, texture
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 HITEM3D_OT_open_website(Operator):
    bl_idname = "hitem3d.open_website"
    bl_label = "Open Hitem3D"
    bl_options = {'REGISTER'}

    def execute(self, context):
        url = getattr(context.scene, "hitem3d_url", "") or "https://www.hitem3d.ai/"
        open_url(url)
        return {'FINISHED'}
class HITEM3D_OT_view_tutorial(Operator):
    bl_idname = "hitem3d.view_tutorial"
    bl_label = "View Tutorial"
    bl_description = "Open the official Hitem3D 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 HITEM3D_OT_open_billing(Operator):
    bl_idname = "hitem3d.open_billing"
    bl_label = "Go Billing"
    bl_options = {'REGISTER'}

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

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

    def execute(self, context):
        scn = context.scene
        ak = getattr(scn, "hitem3d_access_key", "") or ""
        sk = getattr(scn, "hitem3d_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.hitem3d_authorization = f"{token_type} {access_token}"
        scn.hitem3d_confirmed = True
        _write_api_keys_to_text(ak, sk, auth)
        self.report({'INFO'}, "API keys confirmed")

        Balance_url = "https://api.hitem3d.ai/open-api/v1/auth/balance"
        Balance_headers = {
            "Authorization": scn.hitem3d_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.hitem3d_balance = float(balance)

        return {'FINISHED'}

def _write_api_keys_to_text(ak: str, sk: str, auth: str):
    name = "Hitem3D_API_Keys.txt"
    addon_dir = os.path.dirname(os.path.realpath(__file__))
    key_file = os.path.join(addon_dir, name)
    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):
    name = "Hitem3D_API_Keys.txt"
    addon_dir = os.path.dirname(os.path.realpath(__file__))
    key_file = os.path.join(addon_dir, name)
    if not os.path.exists(key_file):
        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()
    
    scn = bpy.context.scene
    if scn:
        if ak:
            scn.hitem3d_access_key = ak
        if sk:
            scn.hitem3d_secret_key = sk
        if auth:
            scn.hitem3d_authorization = auth

    return True

class HITEM3D_OT_set_task(Operator):
    bl_idname = "hitem3d.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.hitem3d_task_type = self.task
        return {'FINISHED'}

class HITEM3D_OT_set_model_type(Operator):
    bl_idname = "hitem3d.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.hitem3d_model_type = self.type
        return {'FINISHED'}

class HITEM3D_OT_select_image(Operator):
    bl_idname = "hitem3d.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.hitem3d_input_image = img
        return {'FINISHED'}

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


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

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

        input_image = getattr(context.scene, "hitem3d_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, "hitem3d_input_image_path", "")
            # If image object exists but path is empty, it's an error for our file-based API
            if getattr(context.scene, "hitem3d_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.hitem3d_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, "hitem3d_input_image", None)
            item.image_path = getattr(context.scene, "hitem3d_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"

        context.scene.hitem3d_is_generating = True
        thread = threading.Thread(target=generation, args=(context, ttype, uid))
        thread.start()

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

        bpy.app.timers.register(reset_generating_flag, first_interval=3.0)

        return {'FINISHED'}

class LoadBaseImageOperator(Operator):
    bl_idname = "hitem3d.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": "hitem3d_input_image",
                    "load_texture_image": "hitem3d_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 = {
                "hitem3d_input_image": "hitem3d_input_image_path",
                "hitem3d_texture_image": "hitem3d_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 = "hitem3d.load_image"
    target_prop = "hitem3d_input_image"

class LoadTextureImageOperator(LoadBaseImageOperator):
    bl_idname = "hitem3d.load_texture_image"
    target_prop = "hitem3d_texture_image"

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

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

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

class LoadBackImageOperator(LoadBaseImageOperator):
    bl_idname = "hitem3d.load_back_image"
    target_prop = "back_image"
    
class HITEM3D_OT_add_texture_task(Operator):
    bl_idname = "hitem3d.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.hitem3d_last_added_object_name = ""
            scene.hitem3d_selected_texture_task_id = ""
            scene.hitem3d_selected_texture_task_type = ""
            scene.hitem3d_selected_texture_task_create_time = ""
            self.report({'WARNING'}, "Texture Generation · Selection. Select one object to get texture...")
            return {'CANCELLED'}
        scene.hitem3d_last_added_object_name = obj.name
        gen_tasks = scene.hitem3d_tasks
        tex_tasks = scene.hitem3d_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.hitem3d_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.hitem3d_selected_texture_task
                for j in reversed(range(len(coll))):
                    coll.remove(j)
                item = coll.add()
                item.index = found_idx
                item.type = "TEX"
                scene.hitem3d_selected_texture_task_index = found_idx
                self.report({'INFO'}, f"Selected existing texture task for {obj.name}.")
            else:
                coll = scene.hitem3d_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 Hitem3D yet. It will be added to Hitem3D.")
        
        selected_texture_task_coll = getattr(scene, "hitem3d_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.hitem3d_selected_texture_task_id = target.task_id
            scene.hitem3d_selected_texture_task_type = target.task_type
            scene.hitem3d_selected_texture_task_create_time = target.create_time
            if selected_texture_task.type == "GEN":
                scene.hitem3d_texture_image = target.image
                scene.hitem3d_texture_image_path = target.image_path
            else:
                scene.hitem3d_texture_image = None
                scene.hitem3d_texture_image_path = ""
        else:
            scene.hitem3d_selected_texture_task_id = ""
            scene.hitem3d_selected_texture_task_type = ""
            scene.hitem3d_selected_texture_task_create_time = ""
            scene.hitem3d_texture_image = None
            scene.hitem3d_texture_image_path = ""
        
        return {'FINISHED'}

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

    def execute(self, context):
        scn = context.scene
        last_name = getattr(scn, "hitem3d_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, "hitem3d_selected_texture_task_id", "")
        if task_id == "":
            task_id = str(uuid.uuid4())

        if scn.hitem3d_texture_image is None:
            self.report({'WARNING'}, "No texture image has been uploaded.")
            return {'CANCELLED'}
        
        # Validate texture image file
        path = getattr(scn, "hitem3d_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.hitem3d_texture_tasks
        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.hitem3d_texture_image
            target.image_path = scn.hitem3d_texture_image_path
        else:
            target.create_time = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
            target.obj = obj
            target.image = scn.hitem3d_texture_image
            target.image_path = scn.hitem3d_texture_image_path
        target.tex_status = "INIT"
        self.report({'INFO'}, f"Generating texture for {obj.name}.")
        
        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.hitem3d_is_generating_texture = True
        auth = getattr(scn, "hitem3d_authorization", "")
        
        thread = threading.Thread(target=texture, args=(auth, tmp_path, scn.hitem3d_texture_image_path, target))
        thread.start()

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

        bpy.app.timers.register(reset_generating_texture_flag, first_interval=3.0)

        return {'FINISHED'}

class HITEM3D_OT_reload_texture_image(Operator):
    bl_idname = "hitem3d.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, "hitem3d_texture_tasks", [])
        idx = getattr(scene, "hitem3d_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.hitem3d_texture_image_path = path
        auth = getattr(scene, "hitem3d_authorization", "")

        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.hitem3d_is_replacing_texture = True
        
        thread = threading.Thread(target=texture, args=(auth, tmp_path, scene.hitem3d_texture_image_path, tasks[idx]))
        thread.start()
        
        def reset_replacing_texture_flag():
            scene.hitem3d_is_replacing_texture = False
            return None

        bpy.app.timers.register(reset_replacing_texture_flag, first_interval=3.0)

        return {'FINISHED'}

    def invoke(self, context, event):
        context.window_manager.fileselect_add(self)
        return {'RUNNING_MODAL'}
        
class HITEM3D_OT_show_error(Operator):
    bl_idname = "hitem3d.show_error"
    bl_label = "Hitem3D 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 = (
    HITEM3D_OT_open_website,
    HITEM3D_OT_view_tutorial,
    HITEM3D_OT_open_billing,
    HITEM3D_OT_confirm_api_key,
    HITEM3D_OT_set_task,
    HITEM3D_OT_set_model_type,
    HITEM3D_OT_select_image,
    HITEM3D_OT_generate_model,
    HITEM3D_OT_add_texture_task,
    HITEM3D_OT_generate_texture,
    HITEM3D_OT_reload_texture_image,
    HITEM3D_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)


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)
