#!/usr/bin/env python3

"""Clean all related obsoleted and outdated LXD base images and instances.

This hook ensures only base instances exist for the latest
(craft base metadata version, remote image provider, remote image os, remote image version)
tuple. All other bases and below minimal base versions image/instance
will be deleted.

Example:
base-instance-charmcraft-buildd-base-v1.0-craft-com.ubuntu.cloud-buildd-core22
                                       |          |                 |      |
craft base metadata version: 1.0 -------          |                 |      |
remote image name: craft-com.ubuntu.cloud ---------                 |      |
remote image os: buildd ---------------------------------------------      |
remote image version: core22 -----------------------------------------------
"""

import json
import re
import subprocess
import sys

from datetime import datetime

from charmcraft import snap

PROJECT_NAME = "charmcraft"
MINIMAL_BASE_VERISON = "2.0"

# We need get image info tuple from regex
# (craft base metadata version, remote image provider, remote image os, remote image version)
# base-instance-charmcraft-buildd-base-v1.0-craft-com.ubuntu.cloud-buildd-core22
SUPPORTED_VERSIONS = [
    r"base-instance-" + re.escape(PROJECT_NAME) + r"-\w+-base-v(\d+\.\d+)-([\w\.\-]+)-(\w+)-(\w+)"
]

# snapshot-craft-com.ubuntu.cloud-buildd-core22-charmcraft-buildd-base-v0.0
# base-instance-charmcraft-buildd-base-v10-689fe40991089812a57b
OBSOLETE_VERSIONS = [
    r"snapshot-craft-[\w\.]+-\w+-\w+-" + re.escape(PROJECT_NAME) + r"-\w+-base-v[\d\.]+",
    r"base-instance-" + re.escape(PROJECT_NAME) + r"-(\w+)-base-v([\d\.]+)-\w+",
]


def _delete_lxd_image(image: dict) -> None:
    print(
        f"Removing old image {image['aliases'][0]['name']} ({image['fingerprint']}) "
        f"in LXD {PROJECT_NAME} project..."
    )
    try:
        subprocess.check_output(
            [
                "lxc",
                "--project",
                PROJECT_NAME,
                "image",
                "delete",
                image["fingerprint"],
            ]
        )
    except subprocess.CalledProcessError:
        print(f"Failed to remove LXD image {image['fingerprint']}.", file=sys.stderr)


def _delete_lxd_instance(instance: dict) -> None:
    print(f"Removing old instance {instance['name']} in LXD {PROJECT_NAME} project...")
    try:
        subprocess.check_output(
            ["lxc", "--project", PROJECT_NAME, "delete", "--force", instance["name"]]
        )
    except subprocess.CalledProcessError:
        print(f"Failed to remove LXD instance {instance['name']}.", file=sys.stderr)


def configure_hook_main():
    # Unique valid base instances directory to prevent duplication.
    image_slots = {}

    cfg = snap.get_snap_configuration()
    try:
        snap.validate_snap_configuration(cfg)
    except ValueError as error:
        reason = str(error)
        print(f"Unsupported snap configuration: {reason}.", file=sys.stderr)
        sys.exit(1)

    # Remove only base images in LXD related project
    try:
        lxd_images_json = subprocess.check_output(
            ["lxc", "-f", "json", "--project", PROJECT_NAME, "image", "list"]
        ).decode()
    except FileNotFoundError:
        print("LXD is not installed.", file=sys.stderr)
        return
    except subprocess.CalledProcessError:
        print(f"Project {PROJECT_NAME} does not exist in LXD.", file=sys.stderr)
        return

    lxd_images = json.loads(lxd_images_json)

    for image in lxd_images:
        for alias in image.get("aliases", []):
            # Remove all obsolete base images
            if any(re.fullmatch(obsolete, alias["name"]) for obsolete in OBSOLETE_VERSIONS):
                _delete_lxd_image(image)
                break

    # Remove only base instances in LXD related project, but keep newest one per image slot.
    try:
        lxd_instances_json = subprocess.check_output(
            ["lxc", "-f", "json", "--project", PROJECT_NAME, "list"]
        ).decode()
    except FileNotFoundError:
        print("LXD is not installed.", file=sys.stderr)
        return
    except subprocess.CalledProcessError:
        print(f"Project {PROJECT_NAME} does not exist in LXD.", file=sys.stderr)
        return

    lxd_instances = json.loads(lxd_instances_json)

    for instance in lxd_instances:
        # Try match image.description for instance full name
        match_result = re.fullmatch(
            SUPPORTED_VERSIONS[0], instance.get("expanded_config", {}).get("image.description", "")
        )
        if match_result:
            # (craft base metadata version, image provider, image os, image version)
            image_slot = match_result.group(1, 2, 3, 4)
            if image_slot[0] < MINIMAL_BASE_VERISON:
                _delete_lxd_instance(instance)
                continue

            if image_slot not in image_slots:
                # Image slot has not been recorded, save the created time and instance info
                image_slots[image_slot] = {
                    "time": datetime.fromisoformat(instance["created_at"][:19]),
                    "instance": instance.copy(),
                }
            else:
                # Image slot has been recorded, find and remove older version
                instance_time = datetime.fromisoformat(instance["created_at"][:19])
                if instance_time >= image_slots[image_slot]["time"]:
                    # Current instance newer then the recorded instance, delete recorded one
                    _delete_lxd_instance(image_slots[image_slot]["instance"])

                    # Update the slot record
                    image_slots[image_slot]["time"] = instance_time
                    image_slots[image_slot]["instance"] = instance.copy()
                else:
                    # Current instance older then the recorded instance, delete current one
                    _delete_lxd_instance(instance)

        # Not matched the supported versions, check if match obsolete versions
        elif any(re.fullmatch(obsolete, instance["name"]) for obsolete in OBSOLETE_VERSIONS):
            _delete_lxd_instance(instance)

        # Do nothing if not match any known patterns


if __name__ == "__main__":
    configure_hook_main()
