"""
Proofly Publish — Blender Add-on
Publish your 3D model to Proofly directly from Blender.

Install: Edit → Preferences → Add-ons → Install → select this file → enable "Proofly Publish"
Panel:   3D Viewport → N-panel (press N) → Proofly tab
"""

bl_info = {
    "name": "Proofly Publish",
    "author": "Proofly",
    "version": (1, 4, 2),
    "blender": (3, 6, 0),
    "location": "View3D > N-Panel > Proofly",
    "description": "Publish your 3D model as a draft listing on Proofly Marketplace",
    "category": "Import-Export",
    "doc_url": "https://proofly.store/developers",
}

import bpy
import os
import json
import tempfile
import threading
import urllib.request
import urllib.error

_API_BASE = "https://api.proofly.store"
_CATEGORIES = [
    # Characters & Creatures
    ("characters", "Characters", ""),
    ("realistic-characters", "Realistic Characters", ""),
    ("stylized-characters", "Stylized Characters", ""),
    ("anime-manga", "Anime/Manga", ""),
    ("creatures-monsters", "Creatures & Monsters", ""),
    ("avatars-vtuber", "Avatars / VTuber", ""),
    ("vrchat-avatars", "VRChat Avatars", ""),
    ("robots-mechs", "Robots & Mechs", ""),
    ("aliens", "Aliens", ""),
    ("superheroes", "Superheroes", ""),
    ("base-bodies", "Base Bodies", ""),
    # Game Dev / Digital
    ("game-ready-assets", "Game-Ready Assets", ""),
    ("low-poly", "Low-Poly", ""),
    ("stylized", "Stylized", ""),
    ("pbr-textured", "PBR / Textured", ""),
    ("rigged-models", "Rigged Models", ""),
    ("animated-models", "Animated Models", ""),
    ("game-environments", "Game Environments", ""),
    ("modular-kits", "Modular Kits", ""),
    ("base-meshes", "Base Meshes", ""),
    ("weapon-packs", "Weapon Packs (Game)", ""),
    ("game-props", "Props (Game)", ""),
    ("modular-buildings", "Modular Buildings", ""),
    ("ar-vr", "AR / VR Ready", ""),
    # CGI / Render (Blender-primary)
    ("vfx-particles", "VFX & Particles", ""),
    ("hair-grooming", "Hair & Grooming", ""),
    ("rigs-only", "Rigs Only", ""),
    ("materials-textures", "Materials & Textures", ""),
    ("hdri-environments", "HDRI & Environments", ""),
    ("scanned-photogrammetry", "Scanned / Photogrammetry", ""),
    # 3D Print
    ("miniatures", "Miniatures", ""),
    ("fantasy", "Fantasy", ""),
    ("sci-fi", "Sci-Fi", ""),
    ("animals", "Animals", ""),
    ("vehicles", "Vehicles", ""),
    ("architecture", "Architecture", ""),
    ("jewelry", "Jewelry", ""),
    ("cosplay", "Cosplay", ""),
    ("art-sculpture", "Art & Sculpture", ""),
    ("home-garden", "Home & Garden", ""),
    ("tools-parts", "Tools & Parts", ""),
    ("functional-prints", "Functional Prints", ""),
    ("electronics", "Electronics", ""),
    ("board-games", "Board Games", ""),
    ("tabletop-rpg", "Tabletop RPG", ""),
    ("toys-games", "Toys & Games", ""),
    ("education", "Education", ""),
    ("mechanical-parts", "Mechanical Parts", ""),
    ("nature-plants", "Nature & Plants", ""),
]
_LICENSES = [
    ("personal", "Personal (non-commercial)", "Buyers may use for personal, non-commercial projects only"),
    ("commercial", "Commercial", "Buyers may use in commercial projects"),
    ("extended", "Extended Commercial", "Buyers may use in unlimited commercial projects including games"),
]
_FORMATS = [
    ("stl", "STL", "Export as ASCII STL (universal, best for 3D printing)"),
    ("glb", "GLB (GLTF Binary)", "Export as GLB with materials (best for game assets)"),
    ("obj", "OBJ", "Export as OBJ with MTL materials"),
    ("fbx", "FBX", "Export as FBX (best for game engines: Unity, Unreal)"),
]


# ---------- Operators ----------

class PROOFLY_OT_publish(bpy.types.Operator):
    """Export selected object(s) and upload as a draft listing on Proofly"""
    bl_idname = "proofly.publish"
    bl_label = "Publish Draft to Proofly"
    bl_options = {"REGISTER"}

    def execute(self, context):
        props = context.scene.proofly_props

        if not props.api_key.startswith("pfly_"):
            self.report({"ERROR"}, "Invalid API key — must start with 'pfly_'. Get it at proofly.store/settings/api")
            return {"CANCELLED"}

        if not props.title.strip():
            self.report({"ERROR"}, "Title is required")
            return {"CANCELLED"}

        if context.selected_objects == []:
            self.report({"ERROR"}, "Select at least one mesh object")
            return {"CANCELLED"}

        fmt = props.export_format
        with tempfile.NamedTemporaryFile(suffix=f".{fmt}", delete=False) as tmp:
            tmp_path = tmp.name

        try:
            self._export(context, fmt, tmp_path)
        except Exception as exc:
            self.report({"ERROR"}, f"Export failed: {exc}")
            os.unlink(tmp_path)
            return {"CANCELLED"}

        tags = [t.strip() for t in props.tags.split(",") if t.strip()]
        price_usd = 0.0 if props.is_free else round(props.price_usd, 2)

        # Run upload in a daemon thread so the UI doesn't freeze
        def _upload():
            try:
                result = _post_multipart(
                    url=f"{_API_BASE}/api/enterprise/publish",
                    api_key=props.api_key,
                    file_path=tmp_path,
                    fields={
                        "title": props.title.strip(),
                        "description": props.description.strip(),
                        "category": props.category,
                        "license_code": props.license_code,
                        "is_free": "true" if props.is_free else "false",
                        "price_usd": str(price_usd),
                        "tags": ",".join(tags),
                        **({ "intended_use": props.intended_use } if props.intended_use != "none" else {}),
                    },
                )
                props.last_result = json.dumps(result)
                props.last_error = ""
            except Exception as exc:
                props.last_result = ""
                props.last_error = str(exc)
            finally:
                try:
                    os.unlink(tmp_path)
                except Exception:
                    pass

        t = threading.Thread(target=_upload, daemon=True)
        t.start()

        self.report({"INFO"}, "Uploading to Proofly… check the panel for the result.")
        return {"FINISHED"}

    def _export(self, context, fmt, path):
        if fmt == "stl":
            bpy.ops.wm.stl_export(
                filepath=path,
                check_existing=False,
                export_selected_objects=True,
                ascii_format=False,
                apply_modifiers=True,
            )
        elif fmt == "glb":
            bpy.ops.export_scene.gltf(
                filepath=path,
                check_existing=False,
                use_selection=True,
                export_format="GLB",
                export_apply=True,
            )
        elif fmt == "obj":
            bpy.ops.wm.obj_export(
                filepath=path,
                check_existing=False,
                export_selected_objects=True,
                apply_modifiers=True,
            )
        elif fmt == "fbx":
            bpy.ops.export_scene.fbx(
                filepath=path,
                check_existing=False,
                use_selection=True,
                apply_unit_scale=True,
                apply_scale_options="FBX_SCALE_NONE",
                bake_space_transform=False,
                use_mesh_modifiers=True,
            )
        else:
            raise ValueError(f"Unknown format: {fmt}")


class PROOFLY_OT_open_dashboard(bpy.types.Operator):
    """Open the Proofly dashboard in a browser to review and publish the draft"""
    bl_idname = "proofly.open_dashboard"
    bl_label = "Open Dashboard"
    bl_options = {"REGISTER"}

    url: bpy.props.StringProperty(default="https://proofly.store/dashboard/models")  # type: ignore

    def execute(self, context):
        bpy.ops.wm.url_open(url=self.url)
        return {"FINISHED"}


# ---------- Properties ----------

class ProoflyProps(bpy.types.PropertyGroup):
    api_key: bpy.props.StringProperty(  # type: ignore
        name="API Key",
        description="Your Proofly API key (pfly_...). Get it at proofly.store/settings/api",
        default="",
        subtype="PASSWORD",
    )
    title: bpy.props.StringProperty(  # type: ignore
        name="Title",
        description="Listing title (required)",
        default="",
        maxlen=128,
    )
    description: bpy.props.StringProperty(  # type: ignore
        name="Description",
        description="Describe the model, its uses and technical details",
        default="",
        maxlen=10000,
    )
    category: bpy.props.EnumProperty(  # type: ignore
        name="Category",
        items=_CATEGORIES,
        default="characters",
    )
    license_code: bpy.props.EnumProperty(  # type: ignore
        name="License",
        items=_LICENSES,
        default="personal",
    )
    is_free: bpy.props.BoolProperty(  # type: ignore
        name="Free",
        description="Make this listing free to download",
        default=True,
    )
    price_usd: bpy.props.FloatProperty(  # type: ignore
        name="Price (USD)",
        description="Price in US Dollars",
        default=4.99,
        min=0.50,
        max=9999.00,
        precision=2,
    )
    tags: bpy.props.StringProperty(  # type: ignore
        name="Tags",
        description="Comma-separated tags (e.g. dragon,fantasy,3d-print)",
        default="",
        maxlen=512,
    )
    intended_use: bpy.props.EnumProperty(  # type: ignore
        name="Intended Use",
        description="Primary use case for this model (adds +15 to listing score)",
        items=[
            ("none", "— Not specified —", "Leave blank (can be set in dashboard)"),
            ("game_engine", "Game Engine", "Unity, Unreal, Godot, etc."),
            ("3d_print", "3D Printing", "FDM, resin, SLS"),
            ("vr_ar", "VR / AR", "Meta Quest, Apple Vision, WebXR"),
            ("archviz", "Arch-Viz", "Architecture visualisation & renders"),
            ("animation", "Animation", "Film, TV, motion-graphics rigged assets"),
            ("vtuber", "VTuber / Avatar", "VRChat, VRM, streaming avatars"),
        ],
        default="none",
    )
    export_format: bpy.props.EnumProperty(  # type: ignore
        name="Export Format",
        items=_FORMATS,
        default="stl",
        description="File format to export from Blender",
    )
    last_result: bpy.props.StringProperty(default="")  # type: ignore
    last_error: bpy.props.StringProperty(default="")  # type: ignore


# ---------- Panel ----------

class PROOFLY_PT_panel(bpy.types.Panel):
    bl_label = "Proofly Publish"
    bl_idname = "PROOFLY_PT_main"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = "Proofly"

    def draw(self, context):
        layout = self.layout
        props = context.scene.proofly_props

        # API key
        box = layout.box()
        box.label(text="Account", icon="LOCK")
        box.prop(props, "api_key", text="API Key")
        if not props.api_key:
            row = box.row()
            row.operator("proofly.open_dashboard", text="Get API Key", icon="URL").url = "https://proofly.store/settings/api"

        layout.separator()

        # Listing info
        box = layout.box()
        box.label(text="Listing", icon="PROPERTIES")
        box.prop(props, "title", text="Title")
        box.prop(props, "description", text="Description")
        box.prop(props, "category", text="Category")
        box.prop(props, "license_code", text="License")
        box.prop(props, "tags", text="Tags")
        box.prop(props, "intended_use", text="Intended Use")

        layout.separator()

        # Pricing
        box = layout.box()
        box.label(text="Pricing", icon="FUND")
        box.prop(props, "is_free", text="Free download")
        if not props.is_free:
            box.prop(props, "price_usd", text="Price (USD)")

        layout.separator()

        # Export
        box = layout.box()
        box.label(text="Export", icon="EXPORT")
        box.prop(props, "export_format", text="Format")

        layout.separator()

        # Publish button
        sel_count = len(context.selected_objects)
        row = layout.row()
        row.scale_y = 1.6
        row.enabled = bool(props.api_key and props.title.strip() and sel_count > 0)
        row.operator("proofly.publish", text=f"Publish Draft ({sel_count} obj)", icon="EXPORT")

        if not props.api_key:
            layout.label(text="Enter your API key above", icon="ERROR")
        elif not props.title.strip():
            layout.label(text="Enter a listing title above", icon="ERROR")
        elif sel_count == 0:
            layout.label(text="Select mesh objects first", icon="ERROR")

        # Result
        if props.last_result:
            try:
                data = json.loads(props.last_result)
                if data.get("ok"):
                    item = data.get("item", {})
                    box = layout.box()
                    box.label(text="✓ Draft created!", icon="CHECKMARK")
                    box.label(text=f"ID: {item.get('id')}")
                    if item.get("mark_code"):
                        box.label(text=f"🛡 {item['mark_code']}", icon="LOCKED")
                    row = box.row()
                    row.operator("proofly.open_dashboard", text="Open in Dashboard", icon="URL").url = item.get("dashboard_url", "https://proofly.store/dashboard/models")
                else:
                    layout.label(text=f"Error: {data.get('message', data.get('error'))}", icon="ERROR")
            except Exception:
                layout.label(text="Unexpected response", icon="ERROR")

        if props.last_error:
            layout.label(text=f"Upload error: {props.last_error}", icon="ERROR")


# ---------- HTTP helper ----------

def _post_multipart(url: str, api_key: str, file_path: str, fields: dict) -> dict:
    """Send multipart/form-data POST with a file field named 'file'."""
    boundary = "----ProoflyBlenderBoundary"
    body_parts = []

    for key, value in fields.items():
        body_parts.append(
            f"--{boundary}\r\n"
            f'Content-Disposition: form-data; name="{key}"\r\n\r\n'
            f"{value}\r\n"
        )

    filename = os.path.basename(file_path)
    with open(file_path, "rb") as fh:
        file_data = fh.read()

    body_parts.append(
        f"--{boundary}\r\n"
        f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n'
        f"Content-Type: application/octet-stream\r\n\r\n"
    )
    body = ("".join(body_parts)).encode("utf-8") + file_data + f"\r\n--{boundary}--\r\n".encode("utf-8")

    req = urllib.request.Request(
        url,
        data=body,
        headers={
            "Content-Type": f"multipart/form-data; boundary={boundary}",
            "X-API-Key": api_key,
            "User-Agent": f"ProoflyBlenderAddon/1.4 Blender/{bpy.app.version_string}",
        },
        method="POST",
    )

    try:
        with urllib.request.urlopen(req, timeout=120) as resp:
            return json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as exc:
        body_bytes = exc.read()
        try:
            return json.loads(body_bytes.decode("utf-8"))
        except Exception:
            raise RuntimeError(f"HTTP {exc.code}: {body_bytes[:200]}")


# ---------- Registration ----------

_CLASSES = [
    ProoflyProps,
    PROOFLY_OT_publish,
    PROOFLY_OT_open_dashboard,
    PROOFLY_PT_panel,
]


def register():
    for cls in _CLASSES:
        bpy.utils.register_class(cls)
    bpy.types.Scene.proofly_props = bpy.props.PointerProperty(type=ProoflyProps)


def unregister():
    for cls in reversed(_CLASSES):
        bpy.utils.unregister_class(cls)
    del bpy.types.Scene.proofly_props


if __name__ == "__main__":
    register()
