"""Reproducible fictional vase demo. Tested with Blender 5.2.1 LTS.

Run in a fresh Blender process, never over an unsaved customer scene:
blender --background --factory-startup --python product_scene.py -- ./demo-output
Outputs: editable .blend, PNG comparisons, transparent PNG, and run-manifest.json.
This creates an illustrative product concept, not a dimensionally verified SKU.
"""

import json
import math
import sys
from pathlib import Path

import bpy
from mathutils import Vector

args = sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else []
out = Path(args[0] if args else "./demo-output").resolve()
out.mkdir(parents=True, exist_ok=True)
bpy.ops.object.select_all(action="SELECT")
bpy.ops.object.delete(use_global=False)
scene = bpy.context.scene
scene.render.engine = "CYCLES"
scene.cycles.samples = 32
scene.cycles.use_denoising = True
scene.render.resolution_x = 1440
scene.render.resolution_y = 900
scene.render.resolution_percentage = 100
scene.render.image_settings.file_format = "PNG"
scene.render.image_settings.color_mode = "RGBA"
scene.view_settings.view_transform = "AgX"
scene.world.use_nodes = True
scene.world.node_tree.nodes["Background"].inputs["Color"].default_value = (0.8, 0.84, 0.9, 1)
scene.world.node_tree.nodes["Background"].inputs["Strength"].default_value = 0.18


def material(name, color, roughness):
    mat = bpy.data.materials.new(name)
    mat.use_nodes = True
    bsdf = mat.node_tree.nodes.get("Principled BSDF")
    bsdf.inputs["Base Color"].default_value = (*color, 1)
    bsdf.inputs["Roughness"].default_value = roughness
    return mat


clay = material("Clay blockout", (0.42, 0.42, 0.42), 0.65)
amber = material("Amber satin", (0.42, 0.16, 0.045), 0.28)
ivory = material("Ivory matte", (0.72, 0.68, 0.56), 0.72)
charcoal = material("Charcoal gloss", (0.032, 0.038, 0.046), 0.12)
ground_mat = material("Warm gray ground", (0.64, 0.62, 0.58), 0.8)

# Revolve a closed cross-section: outer wall, lip, inner wall, interior floor.
# Ring samples are deliberately explicit so an author can review the silhouette.
profile = [
    (0.001, 0.02), (0.62, 0.02), (0.67, 0.06), (0.71, 0.22),
    (0.75, 0.55), (0.74, 0.9), (0.63, 1.25), (0.43, 1.55),
    (0.32, 1.8), (0.32, 2.0), (0.30, 2.04), (0.25, 2.04),
    (0.23, 2.0), (0.23, 1.8), (0.34, 1.52), (0.54, 1.22),
    (0.65, 0.88), (0.66, 0.56), (0.62, 0.25), (0.54, 0.14),
    (0.001, 0.14),
]
segments = 96
verts = [(r * math.cos(2 * math.pi * i / segments),
          r * math.sin(2 * math.pi * i / segments), z)
         for r, z in profile for i in range(segments)]
faces = []
for j in range(len(profile) - 1):
    for i in range(segments):
        k = (i + 1) % segments
        faces.append((j * segments + i, j * segments + k,
                      (j + 1) * segments + k, (j + 1) * segments + i))
faces.append(tuple(reversed(range(segments))))
faces.append(tuple((len(profile) - 1) * segments + i for i in range(segments)))
mesh = bpy.data.meshes.new("Vase revolved profile")
mesh.from_pydata(verts, [], faces)
mesh.update()
vase = bpy.data.objects.new("Juniper concept vase", mesh)
scene.collection.objects.link(vase)
for poly in mesh.polygons:
    poly.use_smooth = True
vase.data.materials.append(clay)
sub = vase.modifiers.new("Smooth silhouette", "SUBSURF")
sub.levels = 2
sub.render_levels = 2
bpy.ops.mesh.primitive_plane_add(size=200, location=(0, 0, 0))
ground = bpy.context.object
ground.name = "Ground"
ground.data.materials.append(ground_mat)


def aim(obj, point):
    obj.rotation_euler = (Vector(point) - obj.location).to_track_quat("-Z", "Y").to_euler()


def area(name, location, power, size):
    data = bpy.data.lights.new(name, "AREA")
    data.energy = power
    data.shape = "DISK"
    data.size = size
    obj = bpy.data.objects.new(name, data)
    scene.collection.objects.link(obj)
    obj.location = location
    aim(obj, (0, 0, 1))
    return obj


key = area("Key", (-3, -4, 5), 650, 3)
fill = area("Fill", (4, -1, 3), 160, 3)
rim = area("Rim", (1, 3, 4), 350, 2)
camera_data = bpy.data.cameras.new("Product camera")
camera = bpy.data.objects.new("Product camera", camera_data)
scene.collection.objects.link(camera)
camera.location = (4, -6, 3.4)
camera_data.type = "ORTHO"
camera_data.ortho_scale = 4.5
aim(camera, (0, 0, 1))
scene.camera = camera
manifest = {"blender": bpy.app.version_string, "resolution": [1440, 900],
            "engine": "CYCLES", "samples": 32, "renders": [], "failure_demo": {}}


def render(name, note):
    scene.render.filepath = str(out / (name + ".png"))
    bpy.ops.render.render(write_still=True)
    manifest["renders"].append({"file": name + ".png", "note": note})


render("blockout", "Gray clay silhouette; camera and geometry fixed thereafter.")
vase.data.materials[0] = amber
render("studio", "Amber satin; key size 3, key power 650; fill and rim enabled.")
key.data.size = 0.15
render("hard-light", "Only key area size changed from 3 to 0.15.")
key.data.size = 3
for name, mat in [("ivory", ivory), ("charcoal", charcoal)]:
    vase.data.materials[0] = mat
    render("material-" + name, "Same geometry, lighting, camera; material=" + mat.name)
vase.data.materials[0] = amber
scene.render.film_transparent = True
ground.hide_render = True
render("transparent", "RGBA PNG; transparent film; ground hidden, no ground shadow.")
scene.render.film_transparent = False
ground.hide_render = False

# Controlled failure, separate from the successful examples. Keep the real error.
scene.camera = None
try:
    bpy.ops.render.render(write_still=False)
except RuntimeError as error:
    manifest["failure_demo"] = {"trigger": "scene.camera = None", "error": str(error)}
scene.camera = camera
render("recovered", "Only active camera restored after the deliberate failure.")
bpy.ops.wm.save_as_mainfile(filepath=str(out / "juniper-vase.blend"))
(out / "run-manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
print("DEMO_COMPLETE", json.dumps(manifest))
