"""Deterministic, editable poster renderer. Image visuals are generated separately."""
from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageOps, ImageEnhance
import io, math, re, hashlib, colorsys, os
from pathlib import Path

W,H=1080,1350
FONT_REG=os.getenv('FONT_REGULAR','/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf')
FONT_BOLD=os.getenv('FONT_BOLD','/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf')

def font(px,bold=False):
    p=FONT_BOLD if bold else FONT_REG
    if not Path(p).exists():
        candidates=['/usr/share/fonts/truetype/noto/NotoSans-Bold.ttf' if bold else '/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf','/Library/Fonts/Arial.ttf']
        p=next((x for x in candidates if Path(x).exists()),p)
    try:return ImageFont.truetype(p,px)
    except OSError:return ImageFont.load_default()

def rgb(hexval, default='#6D43C2'):
    text=(hexval or default).lstrip('#')
    if not re.fullmatch(r'[0-9a-fA-F]{6}',text): text=default.lstrip('#')
    return tuple(bytes.fromhex(text))

def blend(a,b,t):return tuple(round(x*(1-t)+y*t) for x,y in zip(a,b))

def contrast(rgbval):return (rgbval[0]*0.299+rgbval[1]*0.587+rgbval[2]*0.114)/255

def palettes(image):
    """Detects dominant colors by pixels (NOT computer vision AI)."""
    img=image.convert('RGB').resize((140,140)).quantize(colors=8,method=Image.Quantize.MEDIANCUT).convert('RGB')
    colors=sorted(img.getcolors(img.width*img.height) or [],reverse=True)
    ordered=[]
    for c, color in colors:
        if color not in ordered and all(sum(abs(color[i]-p[i]) for i in range(3))>70 for p in ordered):
            ordered.append(color)
    non_neutral=[c for c in ordered if max(c)-min(c)>35 and 35<sum(c)/3<235]
    first=non_neutral[0] if non_neutral else (ordered[0] if ordered else (109,67,194))
    second=ordered[1] if len(ordered)>1 else (250,247,255)
    return ['#%02X%02X%02X'%v for v in [first,second]]

def multiline(draw,txt,max_width,face,max_lines=4):
    words=(txt or '').strip().split()
    if not words:return ['']
    lines=[]; cur=''
    for w in words:
        trial=(cur+' '+w).strip()
        if draw.textbbox((0,0),trial,font=face)[2] <= max_width or not cur:cur=trial
        else:lines.append(cur);cur=w
    if cur:lines.append(cur)
    if len(lines)>max_lines:
        lines=lines[:max_lines]
        while draw.textbbox((0,0),lines[-1]+'…',font=face)[2]>max_width and len(lines[-1])>1:lines[-1]=lines[-1][:-1]
        lines[-1]+='…'
    return lines

def draw_fit(draw,text,x,y,width,max_height,top_size,color,bold=True):
    for sz in range(top_size,38,-2):
        f=font(sz,bold)
        line_h=int(sz*1.32)
        lines=multiline(draw,text,width,f,4)
        if len(lines)*line_h<=max_height:
            for line in lines:
                draw.text((x,y),line,font=f,fill=color,stroke_width=0)
                y+=line_h
            return y
    raise ValueError('Headline terlalu panjang untuk desain. Singkatkan judul.')

def render(brand, item, *, visual_bytes=None, reference_path=None, logo_path=None, mode='medium'):
    primary=rgb(brand.get('primary','#6D43C2'))
    sec=rgb(brand.get('secondary','#FFF8F2'), '#FFF8F2')
    dark=blend(primary,(15,18,35),0.48)
    # Render a one-column gradient; stretch natively (far faster for 90 posters).
    stripe=Image.new('RGB',(1,H))
    stripe.putdata([blend(blend(sec,primary,max(0,0.48-(y/H)*0.20)),dark,(y/H)*0.78) for y in range(H)])
    img=stripe.resize((W,H))
    d=ImageDraw.Draw(img)
    # Pattern with deterministic variants, based on topic; not random content duplicated.
    digest=hashlib.sha256(item['title'].encode()).digest()
    for j in range(6):
        px=90+digest[j]*4; py=95+digest[j+6]*4
        r=42+digest[j+12]
        outline=blend(primary,(255,255,255),0.52)
        d.ellipse((px-r,py-r,px+r,py+r),outline=outline,width=3)
    if visual_bytes:
        art=Image.open(io.BytesIO(visual_bytes)).convert('RGB')
        art=ImageOps.fit(art,(W,970),method=Image.Resampling.LANCZOS,centering=(0.5,0.36))
        art=ImageEnhance.Contrast(art).enhance(0.99)
        img.paste(art,(0,0))
        # fade image into the solid dark panel
        fade=Image.new('RGBA',(1,540))
        fade.putdata([(*dark,int(255*(y/540)**1.3)) for y in range(540)])
        overlay=fade.resize((W,540))
        part=img.crop((0,430,W,970)).convert('RGBA')
        img.paste(Image.alpha_composite(part,overlay).convert('RGB'),(0,430))
    else:
        # Branded art composition for the explicit offline mode.
        art=Image.new('RGBA',(W,770),(0,0,0,0)); q=ImageDraw.Draw(art)
        r=260
        q.ellipse((380-r,150-r,380+r,150+r),fill=(*blend(primary,(255,255,255),0.4),130))
        q.rounded_rectangle((250,160,905,690),radius=85,fill=(*blend(primary,(255,255,255),0.30),205))
        q.ellipse((395,100,975,680),fill=(*blend(primary,(255,255,255),0.62),170))
        q.rounded_rectangle((335,230,835,600),radius=48,outline=(255,255,255,245),width=11)
        art=art.filter(ImageFilter.GaussianBlur(1))
        img.paste(Image.alpha_composite(img.crop((0,0,W,770)).convert('RGBA'),art).convert('RGB'),(0,0))
    d=ImageDraw.Draw(img)
    # Backplate for content, deliberately different from image asset layer.
    panel_start=760 if visual_bytes else 735
    d.rounded_rectangle((42,panel_start,W-42,H-42),radius=50,fill=(*dark,),outline=blend(primary,(255,255,255),0.18),width=3)
    d.rounded_rectangle((83,panel_start+44,290,panel_start+92),radius=18,fill=primary)
    cat=(item.get('category') or 'KONTEN').upper()[:18]
    d.text((102,panel_start+53),cat,font=font(22,True),fill='white')
    headline=item.get('title','Konten Baru')
    text_y=panel_start+117
    text_y=draw_fit(d,headline,94,text_y,880,242,76,(255,255,255))
    sub=item.get('subtitle','').strip()
    if sub:
        subfont=font(30)
        for line in multiline(d,sub,862,subfont,3):
            if text_y+40>H-145:break
            d.text((98,text_y+14),line,font=subfont,fill=(232,226,250))
            text_y+=41
    d.line((95,H-127,W-95,H-127),fill=blend(primary,(255,255,255),0.5),width=2)
    d.text((97,H-109),'SWIPE • SAVE • SHARE',font=font(20,True),fill=(230,229,239))
    # Consistent brand name and original uploaded logo.
    brand_name=brand['name'][:34]
    d.rounded_rectangle((47,44,900,140),radius=26,fill=(*dark,))
    bx=72
    if logo_path and Path(logo_path).exists():
        try:
            logo=Image.open(logo_path).convert('RGBA')
            logo.thumbnail((78,78),Image.Resampling.LANCZOS)
            box=Image.new('RGBA',(83,83),(255,255,255,255)); box.alpha_composite(logo,((83-logo.width)//2,(83-logo.height)//2))
            img.paste(box.convert('RGB'),(58,50));bx=161
        except Exception:pass
    d=ImageDraw.Draw(img)
    f=font(33,True)
    while d.textbbox((0,0),brand_name,font=f)[2]>W-bx-120 and f.size>18:
        f=font(f.size-2,True)
    d.text((bx,72),brand_name,font=f,fill=(255,255,255))
    out=io.BytesIO();img.save(out,'JPEG',quality=93,optimize=True)
    return out.getvalue()


def perceptual_dhash(image_bytes):
    """64-bit difference hash. Detects near-identical images, not semantic repeats."""
    img=Image.open(io.BytesIO(image_bytes)).convert('L').resize((9,8),Image.Resampling.LANCZOS)
    pixels=list(img.tobytes());bits=0
    for y in range(8):
        for x in range(8):
            bits=(bits<<1)|int(pixels[y*9+x]>pixels[y*9+x+1])
    return bits

def hash_distance(a,b):
    return (a^b).bit_count()
