#!/usr/bin/env python3
# coding: utf-8
# vi: tabstop=8 expandtab shiftwidth=4 softtabstop=4

import argparse
import requests
import sys
import logging
import re
from datetime import datetime, timezone, timedelta

# Define logging levels and their corresponding formats
LEVELS = {
    "error": "%(asctime)s %(module)s - %(levelname)-8s - %(message)s (func:%(funcName)s line:%(lineno)d)",
    "critical": "%(asctime)s %(module)s - %(levelname)-8s - %(message)s (func:%(funcName)s line:%(lineno)d)",
    "info": "%(asctime)s %(module)s - %(levelname)-8s - %(message)s",
    "debug": "%(asctime)s %(module)s - %(levelname)-8s - %(message)s",
    "warning": "%(asctime)s %(module)s - %(levelname)-8s - %(message)s",
}

class CustomLogger(logging.Logger):
    def __init__(self, name, level, quiet=False, verbose=False):
        super().__init__(name)
        self.setLevel(level)
        self.quiet = quiet
        self.verbose = verbose

    def warning(self, message, *args, **kwargs):
        handler = logging.StreamHandler()
        handler.setFormatter(logging.Formatter(LEVELS["warning"], "%Y-%m-%d %H:%M:%S"))
        self.addHandler(handler)
        super().warning(message, *args, stacklevel=2, **kwargs)
        self.removeHandler(handler)

    def error(self, message, *args, **kwargs):
        handler = logging.StreamHandler()
        handler.setFormatter(logging.Formatter(LEVELS["error"], "%Y-%m-%d %H:%M:%S"))
        self.addHandler(handler)
        super().error(message, *args, stacklevel=2, **kwargs)
        self.removeHandler(handler)

    def critical(self, message, *args, **kwargs):
        handler = logging.StreamHandler()
        handler.setFormatter(logging.Formatter(LEVELS["critical"], "%Y-%m-%d %H:%M:%S"))
        self.addHandler(handler)
        super().critical(message, *args, stacklevel=2, **kwargs)
        self.removeHandler(handler)

    def info(self, message, *args, **kwargs):
        if not self.quiet:
            handler = logging.StreamHandler()
            handler.setFormatter(logging.Formatter(LEVELS["info"], "%Y-%m-%d %H:%M:%S"))
            self.addHandler(handler)
            super().info(message, *args, stacklevel=2, **kwargs)
            self.removeHandler(handler)

    def debug(self, message, *args, **kwargs):
        if self.verbose and not self.quiet:
            handler = logging.StreamHandler()
            handler.setFormatter(logging.Formatter(LEVELS["debug"], "%Y-%m-%d %H:%M:%S"))
            self.addHandler(handler)
            super().debug(message, *args, stacklevel=2, **kwargs)
            self.removeHandler(handler)

def enforce_https(url):
    if url.startswith("http://"):
        url = "https://" + url[7:]
    elif not url.startswith("https://"):
        url = "https://" + url
    return url

def get_collections(session, platform):
    url = f"{platform}/api/v1/search/collections?perPage=0"
    response = session.get(url)
    if not response.ok:
        raise Exception(f"Failed to fetch collections: {response.status_code} {response.text}")
    return response.json()

def delete_collection(session, platform, collection_id):
    url = f"{platform}/api/v1/collections/{collection_id}"
    response = session.delete(url)
    return response.ok, response.text

def pretty_print_matches(matched):
    if not matched:
        return

    logger.info("Matching collections:")

    name_header = "Name"
    id_header = "ID"
    api_header = "APIs"
    date_header = "Last Update"
    age_header = "Days Old"

    name_width = 40
    id_width = max(len(id_header), 36)
    api_width = max(len(api_header), 5)
    date_width = max(len(date_header), 25)
    age_width = max(len(age_header), 8)

    print(f"| {name_header.ljust(name_width)} | {id_header.ljust(id_width)} | {api_header.rjust(api_width)} | {date_header.ljust(date_width)} | {age_header.rjust(age_width)} |")
    print(f"|{'-' * (name_width + 2)}|{'-' * (id_width + 2)}|{'-' * (api_width + 2)}|{'-' * (date_width + 2)}|{'-' * (age_width + 2)}|")

    for name, cid, apis, cdate, age in matched:
        display_name = (name[:37] + "...") if len(name) > 40 else name
        print(f"| {display_name.ljust(name_width)} | {cid.ljust(id_width)} | {str(apis).rjust(api_width)} | {cdate.ljust(date_width)} | {str(age).rjust(age_width)} |")

    print()

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Search or delete API collections')
    parser.add_argument('-q', "--quiet", action="store_true", help="Quiet output - only minimal is shown")
    parser.add_argument('-v', "--verbose", action="store_true", help="Enable verbose output")
    parser.add_argument("-t", "--token", required=True, help="API token for authentication (X-API-KEY header)")
    parser.add_argument('-p', "--platform", required=False, default='https://us.42crunch.cloud', type=enforce_https, help="Platform URL (default https://us.42crunch.cloud)")
    parser.add_argument("-a", "--action", required=True, choices=["search", "delete"], help="Action to perform: search or delete")
    parser.add_argument("-d", "--days", required=False, type=int, help="Match collections last updated before this many days ago")
    parser.add_argument("-n", "--name", required=False, help="Match collections by name (supports wildcard '*')")
    parser.add_argument('-e', "--empty-only", action="store_true", help="Only match collections with zero APIs (empty collections)")
    parser.add_argument('-f', "--force", action="store_true", help="Force deletion without confirmation")

    args = parser.parse_args()

    logger = CustomLogger(__name__, logging.DEBUG, quiet=args.quiet, verbose=args.verbose)

    if args.days is None and args.name is None:
        logger.error("You must specify at least one of --days or --name.")
        sys.exit(1)

    session = requests.Session()
    session.headers.update({
        "X-API-KEY": args.token,
        "Content-Type": "application/json"
    })

    try:
        logger.info("Fetching collections...")
        data = get_collections(session, args.platform)

        now = datetime.now(timezone.utc)

        threshold_date = None
        if args.days is not None:
            threshold_date = now - timedelta(days=args.days)
            logger.info(f"Matching collections last updated before {threshold_date.isoformat()}")

        name_regex = None
        if args.name:
            logger.info(f"Matching collections with name '{args.name}' (wildcard '*' supported)")
            escaped_name = re.escape(args.name)
            pattern = escaped_name.replace(r'\*', '.*')
            name_regex = re.compile(f"^{pattern}$", re.IGNORECASE)

        matched = []
        for item in data.get("list", []):
            last_update_ts = item.get("lastUpdate")
            collection_id = item.get("id")
            collection_name = item.get("name", "")
            api_count = item.get("apiCount", 0)

            if not last_update_ts:
                continue

            match = False
            try:
                last_update_ts = int(last_update_ts)
                last_update_date = datetime.fromtimestamp(last_update_ts, tz=timezone.utc)
                age_days = (now - last_update_date).days

                logger.debug(f"Testing collection '{collection_name}' (ID: {collection_id}) APIs={api_count} last updated {last_update_date.isoformat()} ({age_days} days ago)")

                if threshold_date and last_update_date < threshold_date:
                    logger.debug(f"Collection '{collection_name}' matches lastUpdate filter")
                    match = True

                if name_regex and name_regex.match(collection_name):
                    logger.debug(f"Collection '{collection_name}' matches name pattern '{args.name}'")
                    match = True

                if match:
                    if args.empty_only and api_count != 0:
                        logger.debug(f"Skipping collection '{collection_name}' (ID: {collection_id}) because it has {api_count} APIs")
                        continue
                    matched.append((collection_name, collection_id, api_count, last_update_date.isoformat(), age_days))

            except Exception as e:
                logger.error(f"Failed to parse lastUpdate for collection '{collection_name}': {e}")

        if args.action == "search":
            if len(matched) == 0:
                logger.info("No matching collections found.")
                sys.exit(0)
            pretty_print_matches(matched)

        elif args.action == "delete":
            if len(matched) == 0:
                logger.info("No matching collections found. Nothing to delete.")
                sys.exit(0)

            pretty_print_matches(matched)

            if not args.force:
                confirm = input("Are you sure you want to delete these collections? Type 'y' or 'yes' to continue: ").strip().lower()
                if confirm not in ('y', 'yes'):
                    logger.warning("Deletion aborted by user.")
                    sys.exit(0)

            logger.info(f"Deleting {len(matched)} collections...")
            deleted_count = 0
            for name, cid, apis, cdate, age in matched:
                logger.info(f"Deleting collection '{name}' (ID: {cid}) APIs={apis} last updated {cdate} ({age} days ago)")
                success, result = delete_collection(session, args.platform, cid)
                if success:
                    logger.info(f"Successfully deleted {cid}")
                    deleted_count += 1
                else:
                    logger.error(f"Failed to delete {cid}: {result}")
            logger.info(f"Deletion complete. Total collections deleted: {deleted_count}")

    except Exception as e:
        logger.critical(f"Error occurred: {str(e)}")
        sys.exit(1)
