#!/usr/bin/env python3
import argparse
import shutil
import ipaddress
import difflib
from jinja2 import Environment, FileSystemLoader
from ruamel.yaml import YAML

import sys, os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from module_utils.entity_name_utils import get_entity_name

# Paths to the group-vars files
PORTS_FILE = './group_vars/all/10_ports.yml'
NETWORKS_FILE = './group_vars/all/09_networks.yml'
ROLE_TEMPLATE_DIR = './templates/roles/web-app'
ROLES_DIR = './roles'

yaml = YAML()
yaml.preserve_quotes = True


def load_yaml_with_comments(path):
    with open(path) as f:
        return yaml.load(f)


def dump_yaml_with_comments(data, path):
    with open(path, 'w') as f:
        yaml.dump(data, f)


def get_next_network(networks_dict, prefixlen):
    """Select the next contiguous subnet, based on the highest existing subnet + one network offset."""
    nets = []
    local = networks_dict['defaults_networks']['local']
    for name, info in local.items():
        # info is a dict with 'subnet' key
        net = ipaddress.ip_network(info['subnet'])
        if net.prefixlen == prefixlen:
            nets.append(net)
    if not nets:
        raise RuntimeError(f"No existing /{prefixlen} subnets to base allocation on.")
    nets.sort(key=lambda n: int(n.network_address))
    last = nets[-1]
    offset = last.num_addresses
    next_net = ipaddress.ip_network((int(last.network_address) + offset, prefixlen))
    return next_net


def get_next_port(ports_dict, category):
    """Assign the next port by taking the max existing plus one."""
    loc = ports_dict['ports']['localhost'][category]
    existing = [int(v) for v in loc.values()]
    return (max(existing) + 1) if existing else 1


def prompt_conflict(dst_file):
    print(f"Conflict detected: {dst_file}")
    print("[1] overwrite, [2] skip, [3] merge")
    choice = None
    while choice not in ('1', '2', '3'):
        choice = input("Enter 1, 2, or 3: ").strip()
    return choice


def render_templates(src_dir, dst_dir, context):
    env = Environment(loader=FileSystemLoader(src_dir), keep_trailing_newline=True, autoescape=False)
    env.filters['bool'] = lambda x: bool(x)
    env.filters['get_entity_name'] = get_entity_name

    for root, _, files in os.walk(src_dir):
        rel = os.path.relpath(root, src_dir)
        target = os.path.join(dst_dir, rel)
        os.makedirs(target, exist_ok=True)
        for fn in files:
            tpl = env.get_template(os.path.join(rel, fn))
            rendered = tpl.render(**context)
            out = fn[:-3] if fn.endswith('.j2') else fn
            dst_file = os.path.join(target, out)

            if os.path.exists(dst_file):
                choice = prompt_conflict(dst_file)
                if choice == '2':
                    print(f"Skipping {dst_file}")
                    continue
                if choice == '3':
                    with open(dst_file) as f_old:
                        old_lines = f_old.readlines()
                    new_lines = rendered.splitlines(keepends=True)
                    additions = [l for l in new_lines if l not in old_lines]
                    if additions:
                        with open(dst_file, 'a') as f:
                            f.writelines(additions)
                        print(f"Merged {len(additions)} lines into {dst_file}")
                    else:
                        print(f"No new lines to merge into {dst_file}")
                    continue
                # overwrite
                print(f"Overwriting {dst_file}")
                with open(dst_file, 'w') as f:
                    f.write(rendered)
            else:
                # create new file
                with open(dst_file, 'w') as f:
                    f.write(rendered)


def main():
    # Load dynamic port categories
    ports_data = load_yaml_with_comments(PORTS_FILE)
    categories = list(ports_data['ports']['localhost'].keys())

    parser = argparse.ArgumentParser(
        description="Create or update a Docker Ansible role, and globally assign network and ports with comments preserved"
    )
    parser.add_argument('-a', '--application-id', required=True, help="Unique application ID")
    parser.add_argument('-n', '--network', choices=['24', '28'], required=True, help="Network prefix length (/24 or /28)")
    parser.add_argument('-p', '--ports', nargs='+', choices=categories, required=True, help=f"Port categories to assign (allowed: {', '.join(categories)})")
    args = parser.parse_args()

    app = args.application_id
    role = f"web-app-{app}"
    role_dir = os.path.join(ROLES_DIR, role)

    if os.path.exists(role_dir):
        if input(f"Role {role} exists. Continue? [y/N]: ").strip().lower() != 'y':
            print("Aborting.")
            sys.exit(1)
    else:
        os.makedirs(role_dir)

    # 1) Render all templates with conflict handling
    render_templates(ROLE_TEMPLATE_DIR, role_dir, {'application_id': app, 'role_name': role, 'database_type': 0})
    print(f"→ Templates applied to {role_dir}")

    # 2) Update global networks file, preserving comments
    networks = load_yaml_with_comments(NETWORKS_FILE)
    prefix = int(args.network)
    new_net = get_next_network(networks, prefix)
    networks['defaults_networks']['local'][app] = {'subnet': str(new_net)}
    shutil.copy(NETWORKS_FILE, NETWORKS_FILE + '.bak')
    dump_yaml_with_comments(networks, NETWORKS_FILE)
    print(f"→ Assigned network {new_net} in {NETWORKS_FILE}")

    # 3) Update global ports file, preserving comments
    ports_data = load_yaml_with_comments(PORTS_FILE)
    assigned = {}
    for cat in args.ports:
        loc = ports_data['ports']['localhost'].setdefault(cat, {})
        if app in loc:
            print(f"→ Existing port for {cat} and {app}: {loc[app]}, skipping.")
        else:
            pnum = get_next_port(ports_data, cat)
            loc[app] = pnum
            assigned[cat] = pnum

    if assigned:
        shutil.copy(PORTS_FILE, PORTS_FILE + '.bak')
        dump_yaml_with_comments(ports_data, PORTS_FILE)
        print(f"→ Assigned ports {assigned} in {PORTS_FILE}")
    else:
        print("→ No new ports assigned.")

if __name__ == '__main__':
    main()
