import os
import math
import hashlib
import random
from typing import List, Tuple

from PIL import Image, ImageDraw, ImageFont, ImageFilter

# =========================================================
# CONFIG
# =========================================================

OUTPUT_DIR = "emby_user_thumbs"
IMAGE_SIZE = 512                 # final image size (square)
CORNER_RADIUS = 22               # rounded corners
FONT_SIZE_RATIO = 0.34           # relative to image size
USE_GRADIENTS = True             # True = soft gradient backgrounds
ADD_SUBTLE_SHADOW = False        # set True if you want slight depth
TEXT_COLOUR = (245, 245, 245, 255)

# Optional: point this to a nicer font installed on your system.
# Windows examples:
#   r"C:\Windows\Fonts\segoeuib.ttf"
#   r"C:\Windows\Fonts\bahnschrift.ttf"
#   r"C:\Windows\Fonts\arialbd.ttf"
# Leave as None to use Pillow default fallback search.
FONT_PATH = r"C:\Windows\Fonts\segoeuib.ttf"

# Example user list. Replace with your own, or load from Emby.
USERS = [
    "AC",
    "AV",
    "AS",
    "DN",
    "DC",
    "FTV",
    "LB",
    "MC",
    "PB",
    "PR",
    "PRXX",
    "PC",
    "RH",
    "SC",
    "TH",
    "X",
]

# Background palette pairs for gradients / solids.
# These are chosen to feel fairly modern and similar in spirit
# to app profile tiles.
PALETTE: List[Tuple[Tuple[int, int, int], Tuple[int, int, int]]] = [
    ((28, 148, 33), (33, 120, 39)),     # green
    ((85, 65, 201), (102, 45, 184)),    # purple
    ((170, 72, 157), (141, 60, 134)),   # magenta
    ((91, 116, 139), (111, 128, 149)),  # slate blue
    ((86, 47, 88), (106, 60, 110)),     # plum
    ((54, 108, 63), (63, 120, 71)),     # deep green
    ((41, 58, 73), (52, 70, 85)),       # charcoal blue
    ((238, 146, 12), (113, 56, 180)),   # orange to violet
    ((255, 93, 39), (45, 144, 241)),    # orange to blue
    ((25, 26, 34), (38, 40, 51)),       # dark neutral
]


# =========================================================
# HELPERS
# =========================================================

def ensure_output_dir(path: str) -> None:
    os.makedirs(path, exist_ok=True)


def safe_filename(name: str) -> str:
    cleaned = "".join(c for c in name if c.isalnum() or c in ("-", "_", " ")).strip()
    cleaned = cleaned.replace(" ", "_")
    return cleaned or "user"


def initials_from_name(name: str) -> str:
    name = (name or "").strip()
    if not name:
        return "?"

    # Split on spaces first
    parts = [p for p in name.replace("_", " ").replace("-", " ").split() if p]

    if len(parts) >= 2:
        return (parts[0][0] + parts[1][0]).upper()

    # Handle camel-ish or single-token names like DaveN, PaulR, RobH
    token = parts[0] if parts else name
    uppers = [c for c in token[1:] if c.isupper()]

    if len(token) == 1:
        return token.upper()

    if uppers:
        return (token[0] + uppers[0]).upper()

    return token[:2].upper()


def hash_to_palette_index(name: str, palette_size: int) -> int:
    digest = hashlib.sha256(name.encode("utf-8")).hexdigest()
    return int(digest[:8], 16) % palette_size


def seeded_rng(name: str) -> random.Random:
    digest = hashlib.sha256(name.encode("utf-8")).digest()
    return random.Random(int.from_bytes(digest[:8], "big"))


def load_font(image_size: int) -> ImageFont.FreeTypeFont:
    font_size = int(image_size * FONT_SIZE_RATIO)

    # Try configured font first
    if FONT_PATH and os.path.exists(FONT_PATH):
        try:
            return ImageFont.truetype(FONT_PATH, font_size)
        except Exception:
            pass

    # Common fallbacks
    fallback_fonts = [
        "DejaVuSans-Bold.ttf",
        "Arial Bold.ttf",
        "arialbd.ttf",
        "seguiemj.ttf",   # not ideal, but sometimes present
        "bahnschrift.ttf"
    ]

    for font_name in fallback_fonts:
        try:
            return ImageFont.truetype(font_name, font_size)
        except Exception:
            continue

    # Last resort
    return ImageFont.load_default()


def lerp(a: int, b: int, t: float) -> int:
    return int(a + (b - a) * t)


def blend_colours(
    c1: Tuple[int, int, int],
    c2: Tuple[int, int, int],
    t: float,
) -> Tuple[int, int, int]:
    return (
        lerp(c1[0], c2[0], t),
        lerp(c1[1], c2[1], t),
        lerp(c1[2], c2[2], t),
    )


def make_gradient_background(size: int, c1: Tuple[int, int, int], c2: Tuple[int, int, int]) -> Image.Image:
    """
    Creates a diagonal gradient background.
    """
    img = Image.new("RGBA", (size, size))
    px = img.load()

    for y in range(size):
        for x in range(size):
            # diagonal interpolation
            t = (x + y) / (2 * (size - 1))
            r = lerp(c1[0], c2[0], t)
            g = lerp(c1[1], c2[1], t)
            b = lerp(c1[2], c2[2], t)
            px[x, y] = (r, g, b, 255)

    return img


def make_mesh_gradient_background(
    size: int,
    c1: Tuple[int, int, int],
    c2: Tuple[int, int, int],
    rng: random.Random,
) -> Image.Image:
    """
    Creates a softer, more modern mesh-like gradient with an angled sweep.
    """
    img = Image.new("RGBA", (size, size))
    px = img.load()

    angle = rng.uniform(0, math.pi)
    dx = math.cos(angle)
    dy = math.sin(angle)
    cx = rng.uniform(size * 0.2, size * 0.8)
    cy = rng.uniform(size * 0.2, size * 0.8)

    accent = blend_colours(c1, c2, 0.5)
    accent_strength = rng.uniform(0.12, 0.28)
    radius = size * rng.uniform(0.35, 0.6)

    for y in range(size):
        for x in range(size):
            proj = ((x - cx) * dx + (y - cy) * dy) / size
            t = max(0.0, min(1.0, 0.5 + proj))
            base = blend_colours(c1, c2, t)

            dist = math.hypot(x - cx, y - cy)
            glow = max(0.0, 1.0 - (dist / radius))
            mix = min(1.0, accent_strength * glow)

            px[x, y] = (
                lerp(base[0], accent[0], mix),
                lerp(base[1], accent[1], mix),
                lerp(base[2], accent[2], mix),
                255,
            )

    return img


def make_solid_background(size: int, colour: Tuple[int, int, int]) -> Image.Image:
    return Image.new("RGBA", (size, size), colour + (255,))


def rounded_mask(size: int, radius: int) -> Image.Image:
    mask = Image.new("L", (size, size), 0)
    draw = ImageDraw.Draw(mask)
    draw.rounded_rectangle((0, 0, size - 1, size - 1), radius=radius, fill=255)
    return mask


def add_soft_light_overlay(base: Image.Image) -> Image.Image:
    """
    Adds a subtle glossy highlight to make the tile feel a bit more polished.
    """
    overlay = Image.new("RGBA", base.size, (255, 255, 255, 0))
    draw = ImageDraw.Draw(overlay)

    w, h = base.size
    draw.ellipse((-w * 0.25, -h * 0.35, w * 0.85, h * 0.45), fill=(255, 255, 255, 28))
    overlay = overlay.filter(ImageFilter.GaussianBlur(radius=w // 18))

    return Image.alpha_composite(base, overlay)


def add_modern_shapes(
    base: Image.Image,
    c1: Tuple[int, int, int],
    c2: Tuple[int, int, int],
    rng: random.Random,
) -> Image.Image:
    """
    Adds blurred blobs, rings, and line accents for a more contemporary feel.
    """
    overlay = Image.new("RGBA", base.size, (0, 0, 0, 0))
    draw = ImageDraw.Draw(overlay)
    w, h = base.size

    accent_light = blend_colours(c1, (255, 255, 255), 0.45)
    accent_dark = blend_colours(c2, (8, 12, 18), 0.35)

    for _ in range(rng.randint(2, 4)):
        blob_w = int(w * rng.uniform(0.28, 0.58))
        blob_h = int(h * rng.uniform(0.28, 0.58))
        x = int(rng.uniform(-w * 0.12, w * 0.72))
        y = int(rng.uniform(-h * 0.12, h * 0.72))
        fill = accent_light if rng.random() > 0.45 else accent_dark
        alpha = rng.randint(38, 88)
        draw.ellipse((x, y, x + blob_w, y + blob_h), fill=fill + (alpha,))

    for _ in range(rng.randint(1, 2)):
        ring_size = int(w * rng.uniform(0.2, 0.42))
        x = int(rng.uniform(-w * 0.08, w * 0.82))
        y = int(rng.uniform(-h * 0.08, h * 0.82))
        width = max(3, w // 64)
        draw.ellipse(
            (x, y, x + ring_size, y + ring_size),
            outline=(255, 255, 255, rng.randint(40, 95)),
            width=width,
        )

    for _ in range(rng.randint(2, 4)):
        x1 = int(rng.uniform(0, w))
        y1 = int(rng.uniform(0, h))
        x2 = int(rng.uniform(0, w))
        y2 = int(rng.uniform(0, h))
        draw.line(
            (x1, y1, x2, y2),
            fill=(255, 255, 255, rng.randint(18, 45)),
            width=max(2, w // 128),
        )

    overlay = overlay.filter(ImageFilter.GaussianBlur(radius=max(8, w // 22)))
    return Image.alpha_composite(base, overlay)


def add_text(img: Image.Image, text: str, font: ImageFont.FreeTypeFont) -> Image.Image:
    draw = ImageDraw.Draw(img)
    center = (img.width / 2, img.height / 2)

    if ADD_SUBTLE_SHADOW:
        shadow_offset = max(2, img.width // 128)
        draw.text(
            (center[0] + shadow_offset, center[1] + shadow_offset),
            text,
            font=font,
            fill=(0, 0, 0, 60),
            anchor="mm",
        )

    draw.text(center, text, font=font, fill=TEXT_COLOUR, anchor="mm")
    return img


def create_avatar(name: str, size: int = IMAGE_SIZE) -> Image.Image:
    rng = seeded_rng(name)
    idx = hash_to_palette_index(name, len(PALETTE))
    c1, c2 = PALETTE[idx]

    if USE_GRADIENTS:
        if rng.random() > 0.4:
            bg = make_mesh_gradient_background(size, c1, c2, rng)
        else:
            bg = make_gradient_background(size, c1, c2)
    else:
        bg = make_solid_background(size, c1)

    bg = add_modern_shapes(bg, c1, c2, rng)
    bg = add_soft_light_overlay(bg)

    mask = rounded_mask(size, CORNER_RADIUS)
    rounded = Image.new("RGBA", (size, size), (0, 0, 0, 0))
    rounded.paste(bg, (0, 0), mask)

    font = load_font(size)
    text = initials_from_name(name)
    rounded = add_text(rounded, text, font)

    return rounded


def save_avatar(img: Image.Image, username: str, out_dir: str) -> str:
    filename = f"{safe_filename(username)}.png"
    out_path = os.path.join(out_dir, filename)
    img.save(out_path, format="PNG", optimize=True)
    return out_path


# =========================================================
# OPTIONAL: replace this with a real Emby API call
# =========================================================

def get_emby_users() -> List[str]:
    """
    Replace this function with a real Emby API call if you want.
    For now it returns the USERS list above.
    """
    return USERS


# Example real API version if you want it later:
#
# import requests
#
# def get_emby_users() -> List[str]:
#     emby_url = "http://YOUR-EMBY:8096"
#     api_key = "YOUR_API_KEY"
#     headers = {"X-Emby-Token": api_key}
#     r = requests.get(f"{emby_url}/Users", headers=headers, timeout=30)
#     r.raise_for_status()
#     data = r.json()
#     return [u.get("Name", "").strip() for u in data if u.get("Name")]


# =========================================================
# MAIN
# =========================================================

def main() -> None:
    ensure_output_dir(OUTPUT_DIR)
    users = get_emby_users()

    if not users:
        print("No users found.")
        return

    print(f"Generating avatars for {len(users)} users...\n")

    for user in users:
        avatar = create_avatar(user, IMAGE_SIZE)
        saved = save_avatar(avatar, user, OUTPUT_DIR)
        print(f"Created: {saved}")

    print(f"\nDone. Files saved to: {os.path.abspath(OUTPUT_DIR)}")


if __name__ == "__main__":
    main()
