import bpy, math
from math import sin, pi, sqrt

bpy.ops.object.select_all(action='SELECT'); bpy.ops.object.delete()

L = 3.0
def sdx(y): return 0.32*sin((y+1.5)*(2*pi/L))

# BODY
bpy.ops.mesh.primitive_uv_sphere_add(segments=48, ring_count=32)
body = bpy.context.active_object
body.name = "Goldfish"
body.scale = (0.55, 1.5, 0.78)
bpy.ops.object.transform_apply(scale=True)
for v in body.data.vertices:
    t=(v.co.y+1.5)/L
    if t>0.55:
        f=1.0-0.35*((t-0.55)/0.45); v.co.x*=f; v.co.z*=f
    v.co.x += sdx(v.co.y)
body.modifiers.new("sub","SUBSURF").levels=2

def mesh_obj(name, verts, faces, thick):
    m=bpy.data.meshes.new(name); m.from_pydata(verts,[],faces)
    o=bpy.data.objects.new(name,m); bpy.context.scene.collection.objects.link(o)
    sol=o.modifiers.new("sol","SOLIDIFY"); sol.thickness=thick
    return o

# TAIL
R,C=14,20
verts=[];faces=[]
for i in range(R):
    t=i/(R-1); y=1.35+t*2.4
    h=0.10+1.30*(t**1.25)
    for j in range(C):
        u=j/(C-1); z=-h+2*h*u
        if i==R-1: z+=0.16*sin(u*2*pi*1.5)
        verts.append((sdx(y),y,z))
for i in range(R-1):
    for j in range(C-1):
        a=i*C+j; faces.append((a,a+1,a+C+1,a+C))
tail=mesh_obj("TailFin",verts,faces,0.03)

# DORSAL
rows=[]
for i in range(10):
    t=i/9; y=-0.7+t*1.7
    bz=0.72*sqrt(max(0.001,1-(abs(y)/1.55)**2))*0.98
    tip=bz+0.55*(sin(pi*min(1,t*1.1))**0.8)
    if i==9: tip+=0.07*sin(t*9)
    rows.append(((sdx(y),y,bz),(sdx(y),y,tip)))
verts=[v for r in rows for v in r]
faces=[(i,i+1,i+3,i+2) for i in range(0,len(verts)-3,2)]
dorsal=mesh_obj("DorsalFin",verts,faces,0.025)

# PECTORALS + EYES
for sx in (1,-1):
    bpy.ops.mesh.primitive_circle_add(vertices=24, radius=0.38, fill_type='NGON')
    p=bpy.context.active_object
    p.scale=(1,0.55,1); bpy.ops.object.transform_apply(scale=True)
    p.location=(sx*0.52+sdx(-0.55), -0.55, -0.12)
    p.rotation_euler=(-0.9, sx*0.9, sx*-0.5)
    p.modifiers.new("sol","SOLIDIFY").thickness=0.02
    p.name="PectoralFin.L" if sx>0 else "PectoralFin.R"
    bpy.ops.mesh.primitive_uv_sphere_add(segments=24, ring_count=16, radius=0.145,
        location=(sx*0.43+sdx(-0.95), -0.95, 0.17))
    e=bpy.context.active_object
    e.name="Eye.L" if sx>0 else "Eye.R"

for o in bpy.context.scene.objects: pass
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.shade_smooth()
result={"objects":[o.name for o in bpy.data.objects]}