#!/usr/bin/env python3
"""Meridian concept Facebook post banner generator (no-text brand frame + image well).

Builds three sizes from the brand kit:
  - 1200x630  (FB feed landscape / link post)
  - 1080x1080 (FB/IG square)
  - 1080x1350 (FB/IG portrait 4:5)

Layout: neutral image well on top (where the property/news picture is dropped),
fixed brand band at the bottom: yellow accent line, Meridian logo (light variant),
PPRA stamp in a corner (compliance), Sebastiaan's bust photo as a circular badge
(personal brand without text). Logo rules honoured: logo NOT in top corners;
PPRA in a corner, separate from the logo.

Paths (rewired 2026-10-26):
  - Assets resolve to the CANONICAL in-bundle copies first
    (okf-kamstra-handbook/X.Resources/MeridianRealty/), falling back to the host
    mirror /opt/data/brand-kit/ — never assume the mirror alone.
  - Outputs are written INSIDE this bundle: brandkit/assets/generated/banners/.
  - Brand fonts live at /opt/data/fonts/meridian/ (see brand_font()); the frame is
    text-free by design, so fonts are only needed when you add text.
"""
import os
from PIL import Image, ImageDraw, ImageFont

# ---- paths -----------------------------------------------------------------
SCRIPTS = os.path.dirname(os.path.abspath(__file__))
BUNDLE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(SCRIPTS))))  # okf-kamstra-handbook/
BRANDKIT = os.path.join(BUNDLE, "C.Purpose", "c2.Marketing", "brandkit")
CANON = os.path.join(BUNDLE, "X.Resources", "MeridianRealty")   # canonical in-bundle assets
HOST_KIT = "/opt/data/brand-kit"                                # host mirror (fallback)
FONT_DIR = "/opt/data/fonts/meridian"                           # brand fonts (CI archive)
OUT = os.path.join(BRANDKIT, "assets", "generated", "banners")
AIDIR = os.path.join(BRANDKIT, "assets", "generated", "ai-images")
os.makedirs(OUT, exist_ok=True)

def asset(*parts):
    """Resolve a brand asset: canonical in-bundle copy first, host mirror second."""
    for base in (CANON, HOST_KIT):
        p = os.path.join(base, *parts)
        if os.path.exists(p):
            return p
    raise FileNotFoundError("brand asset not found in bundle or host mirror: " + os.path.join(*parts))

def brand_font(weight="roman", size=48):
    """Brand font loader — Helvetica Neue LT Std from the CI archive, never a lookalike.

    weight: black | bold | roman | kindness. (The CI's 45 Light and the condensed
    weights were NOT supplied with the font archive — substitute `roman` and say so
    in the design notes rather than pulling an unlicensed font.)
    """
    names = {
        "black":    "HelveticaNeueLTStd-95-Black.otf",
        "bold":     "HelveticaNeueLTStd-75-Bold.otf",
        "roman":    "HelveticaNeueLTStd-55-Roman.otf",
        "kindness": "KindnessThings.ttf",
    }
    if weight not in names:
        raise ValueError("unknown weight %r; use one of %s" % (weight, sorted(names)))
    path = os.path.join(FONT_DIR, names[weight])
    if os.path.exists(path):
        return ImageFont.truetype(path, size)
    fallback = "/opt/data/fonts/liberation/LiberationSans-Regular.ttf"  # never die on a missing font
    print("WARN: brand font %s missing — falling back to %s" % (names[weight], fallback))
    return ImageFont.truetype(fallback, size)

BLUE = (32, 52, 98)        # #203462
YELLOW = (248, 165, 56)    # #F8A538
WHITE = (255, 255, 255)
WELL_TINT = (232, 235, 242)  # light blue-grey image well

LOGO = asset("meridian-logo-horizontal-colour-dark.png")  # 'dark' = white-text variant (pixel-verified 2026-08-24)
PPRA = asset("meridian-ppra-stamp.png")                   # white knockout — the only stamp artwork on file
BUST = os.path.join(HOST_KIT, "Photos", "DSCF1958.jpg")   # example bust photo (host mirror only)

# Face-and-shoulders crop box for DSCF1958.jpg (4000x6000), measured 2026-08-24:
# face centre ~51% x / ~34% y; head height ~24% of frame height; shoulders ~50%;
# laptop/table below ~65%. Tight crop: face large, shoulders at bottom edge.
# Square side = 42% of frame HEIGHT (= 63% of width); top at ~10%; centred on face x.
BUST_CROP = {"cx": 0.51, "cy": 0.36, "side_h": 0.42}  # centre + side as fraction of H

def circle_crop(img, size):
    """Tight face-and-shoulders square crop, then circular mask. Ratio-safe."""
    W, H = img.size
    side = int(min(H * BUST_CROP["side_h"], W))
    cx, cy = int(W * BUST_CROP["cx"]), int(H * BUST_CROP["cy"])
    x0 = max(0, min(cx - side // 2, W - side))
    y0 = max(0, min(cy - side // 2, H - side))
    sq = img.crop((x0, y0, x0 + side, y0 + side))
    sq = sq.resize((size, size), Image.LANCZOS)
    mask = Image.new("L", (size, size), 0)
    d = ImageDraw.Draw(mask)
    d.ellipse((0, 0, size, size), fill=255)
    out = Image.new("RGBA", (size, size), (0, 0, 0, 0))
    out.paste(sq, (0, 0), mask)
    return out, mask

def rounded_rect(draw, box, radius, fill, outline=None, width=0):
    draw.rounded_rectangle(box, radius=radius, fill=fill,
                           outline=outline, width=width)

def house_icon(s, color, alpha):
    """Simple house line icon (no text) to mark the image well. Returns RGBA image."""
    col = color + (int(alpha),)
    r = Image.new("RGBA", (s * 2, s * 2), (0, 0, 0, 0))
    dr = ImageDraw.Draw(r)
    dr.line([(s*0.5, s*1.1), (s*0.5, s*0.45), (s, s*0.12), (s*1.5, s*0.45), (s*1.5, s*1.1)], fill=col, width=max(3, s//14), joint="curve")
    dr.rectangle([(s*0.72, s*0.62), (s*1.28, s*1.1)], outline=col, width=max(3, s//14))
    return r

def build(size, well_bbox, band_top, badge_size, logo_w, name, well_image=None):
    W, H = size
    canvas = Image.new("RGB", (W, H), WHITE)

    # --- image well (space for post picture) ---
    well = Image.new("RGBA", (W, H), (0, 0, 0, 0))
    dw = ImageDraw.Draw(well)
    rounded_rect(dw, well_bbox, radius=18, fill=WELL_TINT,
                 outline=BLUE + (60,), width=3)
    if well_image:
        # cover-fit the post picture into the well, rounded corners
        wim = Image.open(well_image).convert("RGB")
        ww, wh = well_bbox[2] - well_bbox[0], well_bbox[3] - well_bbox[1]
        scale = max(ww / wim.width, wh / wim.height)
        wim = wim.resize((int(wim.width * scale) + 1, int(wim.height * scale) + 1), Image.LANCZOS)
        left = (wim.width - ww) // 2
        top = (wim.height - wh) // 2
        wim = wim.crop((left, top, left + ww, top + wh))
        mask = Image.new("L", (ww, wh), 0)
        ImageDraw.Draw(mask).rounded_rectangle((0, 0, ww, wh), radius=18, fill=255)
        well.paste(wim, (well_bbox[0], well_bbox[1]), mask)
    else:
        hx, hy = (well_bbox[0] + well_bbox[2]) // 2, (well_bbox[1] + well_bbox[3]) // 2
        s = min(90, int((well_bbox[2]-well_bbox[0])*0.06))
        icon = house_icon(s, BLUE, 110)
        well.paste(icon, (int(hx - s), int(hy - s)), icon)
    canvas.paste(well, (0, 0), well)

    # --- brand band ---
    band = Image.new("RGBA", (W, H), (0, 0, 0, 0))
    db = ImageDraw.Draw(band)
    db.rectangle([0, band_top, W, H], fill=BLUE + (255,))
    db.rectangle([0, band_top, W, band_top + max(5, H // 110)], fill=YELLOW + (255,))
    canvas.paste(band, (0, 0), band)

    # --- logo (light variant, centred) ---
    logo = Image.open(LOGO).convert("RGBA")
    lw = logo_w
    lh = int(logo.height * lw / logo.width)
    logo = logo.resize((lw, lh), Image.LANCZOS)
    lx = (W - lw) // 2
    ly = band_top + (H - band_top - lh) // 2 + max(5, H // 90)
    canvas.paste(logo, (lx, ly), logo)

    # --- PPRA stamp (bottom-left corner of band, compliance) ---
    ppra = Image.open(PPRA).convert("RGBA")
    pw = int(W * 0.13)
    ph = int(ppra.height * pw / ppra.width)
    ppra = ppra.resize((pw, ph), Image.LANCZOS)
    canvas.paste(ppra, (int(W * 0.035), H - ph - int(H * 0.025)), ppra)

    # --- Sebastiaan bust badge (personal brand, overlapping band top edge, right) ---
    bust = Image.open(BUST)
    badge_size = max(badge_size, int(H * 0.18))  # prominent: >=18% of canvas height
    badge, mask = circle_crop(bust, badge_size)
    bx = W - badge_size - int(W * 0.04)
    by = band_top - badge_size // 2
    ring = Image.new("RGBA", (badge_size, badge_size), (0, 0, 0, 0))
    dr = ImageDraw.Draw(ring)
    dr.ellipse((0, 0, badge_size, badge_size), outline=YELLOW + (255,), width=max(4, badge_size // 32))
    canvas.paste(ring, (bx, by), ring)
    canvas.paste(badge, (bx, by), mask)

    path = name if os.path.isabs(name) else os.path.join(OUT, name)
    os.makedirs(os.path.dirname(path), exist_ok=True)
    canvas.save(path, "PNG")
    print(f"saved {path} ({W}x{H})")
    return path

def main():
    # 1200x630 landscape
    build((1200, 630), (40, 40, 1160, 380), 440, 140, 430, "meridian-banner-concept-1200x630.png")
    # 1080x1080 square
    build((1080, 1080), (40, 40, 1040, 780), 840, 150, 400, "meridian-banner-concept-1080x1080.png")
    # 1080x1350 portrait
    build((1080, 1350), (40, 40, 1040, 1010), 1080, 160, 420, "meridian-banner-concept-1080x1350.png")

    # --- demo variants with AI-generated catchy images in the well ---
    build((1200, 630), (40, 40, 1160, 380), 440, 140, 430, "meridian-banner-demo-1200x630.png",
          well_image=os.path.join(AIDIR, "pretoria-16x9.png"))
    build((1080, 1080), (40, 40, 1040, 780), 840, 150, 400, "meridian-banner-demo-1080x1080.png",
          well_image=os.path.join(AIDIR, "waterkloof-1x1.png"))
    build((1080, 1350), (40, 40, 1040, 1010), 1080, 160, 420, "meridian-banner-demo-1080x1350.png",
          well_image=os.path.join(AIDIR, "menlo-park-4x5-0.png"))
    print("DONE")

if __name__ == "__main__":
    main()
