#!/usr/bin/env python3
"""
Detects and removes radio cover image assets that are visually identical to the
generic default "R" placeholder logo (regenerated over time with different
encoders/quality, so exact byte/MD5 matching is not reliable -- this uses a
perceptual hash instead).

Usage:
    python3 remove_placeholder_covers.py            # dry run, prints report
    python3 remove_placeholder_covers.py --delete    # actually delete matches
    python3 remove_placeholder_covers.py --delete --log removed.log

Directories scanned (all under ASSETS_DIR, whichever of these exist):
    .    (flat/root cover, e.g. radio20718_1513957948.jpg -- mirrors xl)
    xl/  (original/extra-large cover)
    lg/  (large thumbnail)
    md/  (medium thumbnail)
    sm/  (small thumbnail)

A filename is deleted from every one of the above directories where it exists,
as soon as ANY of its size variants matches the placeholder (all sizes are
regenerated/copied from the same source image, so a match in one implies the
others are the same logo too).
"""
import argparse
import os
import sys
from datetime import datetime

from PIL import Image

ASSETS_DIR = "/var/www/html/appradio/radio/assets"
SUBDIRS = ["", "xl", "lg", "md", "sm"]

# dHash (16x16 -> 256 bit) of the known placeholder, precomputed from
# assets/radio20718_1513957948.jpg. Baked in so this script keeps working
# even after the reference file itself has been cleaned up.
REFERENCE_DHASH = 0x40783FF0FE40E078C230CE30CC60CC68C4B0C658CE18C638ECC8F9727FD68078

# Empirically: true matches land at distance <= 34 (out of 256 bits); the
# nearest unrelated cover image sits at distance 75. 50 leaves comfortable
# margin on both sides.
DISTANCE_THRESHOLD = 50
HASH_SIZE = 16


def dhash(path, hash_size=HASH_SIZE):
    with Image.open(path) as im:
        im = im.convert("L").resize((hash_size + 1, hash_size), Image.LANCZOS)
        pixels = list(im.getdata())
        w = hash_size + 1
        val = 0
        for row in range(hash_size):
            for col in range(hash_size):
                bit = 1 if pixels[row * w + col] > pixels[row * w + col + 1] else 0
                val = (val << 1) | bit
        return val


def hamming(a, b):
    return bin(a ^ b).count("1")


def find_matches():
    """Returns a sorted set of base filenames (as found in the root dir)
    that match the placeholder in any of root/lg/md."""
    matched = set()
    errors = []

    for sub in SUBDIRS:
        d = os.path.join(ASSETS_DIR, sub) if sub else ASSETS_DIR
        if not os.path.isdir(d):
            continue
        for name in os.listdir(d):
            path = os.path.join(d, name)
            if not name.startswith("radio") or not os.path.isfile(path):
                continue
            try:
                h = dhash(path)
            except Exception as e:
                errors.append((path, str(e)))
                continue
            if hamming(h, REFERENCE_DHASH) <= DISTANCE_THRESHOLD:
                matched.add(name)

    return sorted(matched), errors


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--delete", action="store_true",
                         help="Actually delete matched files (default: dry run only)")
    parser.add_argument("--log", default=None,
                         help="Append a record of deleted files to this log file")
    args = parser.parse_args()

    matched, errors = find_matches()

    print(f"Found {len(matched)} radio ID(s) using the placeholder cover:")
    for name in matched:
        print(f"  {name}")

    if errors:
        print(f"\n{len(errors)} file(s) could not be read (skipped):")
        for path, err in errors:
            print(f"  {path}: {err}")

    if not matched:
        print("\nNothing to do.")
        return

    if not args.delete:
        print("\nDry run only -- no files deleted. Re-run with --delete to remove them.")
        return

    removed = []
    for sub in SUBDIRS:
        d = os.path.join(ASSETS_DIR, sub) if sub else ASSETS_DIR
        for name in matched:
            path = os.path.join(d, name)
            if os.path.isfile(path):
                os.remove(path)
                removed.append(path)

    print(f"\nDeleted {len(removed)} file(s).")

    if args.log:
        with open(args.log, "a") as f:
            f.write(f"\n# run at {datetime.now().isoformat()}\n")
            for path in removed:
                f.write(path + "\n")


if __name__ == "__main__":
    sys.exit(main())
