# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
# Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.

from flask import (
    g, render_template, url_for, flash, redirect, request, jsonify
)
from ogcp.forms.action_forms import (
    WOLForm, SetupForm, ClientDetailsForm, ImageDetailsForm, HardwareForm,
    SessionForm, ImageRestoreForm, ImageCreateForm, SoftwareForm, BootModeForm,
    RoomForm, DeleteRoomForm, CenterForm, DeleteCenterForm, OgliveForm,
    GenericForm, SelectClientForm, ImageUpdateForm, ImportClientsForm,
    ServerForm, DeleteRepositoryForm, RepoForm, FolderForm, CacheForm,
    ClientMoveForm, RunScriptForm, ImageConfigForm, ImageFetchForm,
    ServerConfigurationForm, SetRepoForm, RunCmdForm
)
from flask_login import (
    current_user, LoginManager,
    login_user, logout_user,
    login_required
)

from pathlib import Path

from ogcp.models import User
from ogcp.forms.auth import LoginForm, UserForm, DeleteUserForm, EditUserForm
from ogcp.og_server import OGServer, servers, ServerError
from flask_babel import lazy_gettext as _l
from flask_babel import gettext, _
from ogcp import app, ogcp_cfg_path
from wtforms import StringField
from functools import wraps
import unicodedata
import ipaddress
import requests
import datetime
import hashlib
import json
import os
import re
from collections import deque

FS_CODES = {
    0: 'DISK',
    1: 'EMPTY',
    2: 'CACHE',
    6: 'EXT4',
    9: 'FAT32',
    13: 'NTFS',
    18: 'EXFAT',
    19: 'LINUX-SWAP'
}

PART_TYPE_CODES = {
    0: 'EMPTY',
    1: 'DISK',
    5: 'EXTENDED',
    7: 'NTFS',
    11: 'FAT32',
    23: 'HNTFS',
    27: 'HFAT32',
    39: 'HNTFS-WINRE',
    130: 'LINUX-SWAP',
    131: 'LINUX',
    142: 'LINUX-LVM',
    202: 'CACHE',
    218: 'DATA',
    239: 'EFI',
    253: 'LINUX-RAID',
    1792: 'NTFS',
    9984: 'WIN-RECOV',
    33280: 'LINUX-SWAP',
    33536: 'LINUX',
    36352: 'LINUX-LVM',
    51712: 'CACHE',
    61184: 'EFI',
    64768: 'LINUX-RAID',
    65535: 'UNKNOWN'
}

def get_invalid_image_partition_types():
    return ['EMPTY', 'LINUX-SWAP', 'CACHE', 'EFI']

PART_SCHEME_CODES = {
    0: 'EMPTY',
    1: 'MSDOS',
    2: 'GPT'
}

login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'

def handle_server_errors(redirect_endpoint):
    def decorator(view_func):
        @wraps(view_func)
        def wrapper(*args, **kwargs):
            try:
                return view_func(*args, **kwargs)
            except ServerError:
                return redirect(url_for(redirect_endpoint))
        return wrapper
    return decorator

def split_csv(s):
     return re.split(r'\s*,\s*', s)

def normalize_mac(mac):
    return mac.replace(':', '').replace('-', '').replace('.', '').lower()

def is_valid_normalized_mac(mac):
    if len(mac) != 12:
        return False
    if not all(c in '0123456789abcdef' for c in mac):
        return False
    return True

def prettify_mac(mac):
    normalized_mac = normalize_mac(mac)

    if not is_valid_normalized_mac(normalized_mac):
        return mac
    return (':'.join(normalized_mac[i:i+2] for i in range(0, 12, 2))).lower()

def is_valid_ip(ip):
    try:
        ipaddress.ip_address(ip)
    except:
        return False
    return True

def remove_accents(text):
    normalized_text = unicodedata.normalize('NFD', text)
    return ''.join(c for c in normalized_text if unicodedata.category(c) != 'Mn')

def validate_elements(elements, min_len=1, max_len=float('inf')):
    valid = True
    if len(elements) < min_len:
        flash(_('Please, select at least {} element(s)').format(min_len),
              category='error')
        valid = not valid
    elif len(elements) > max_len:
        flash(_('No more than {} element(s) can be selected for the given action').format(max_len),
              category='error')
        valid = not valid
    return valid

def parse_elements(checkboxes_dict):
    unwanted_elements = ['csrf_token', 'scope-server', 'scope-center',
                         'scope-room', 'image-server', 'repos-server', 'folder']
    elements = set()
    for key, elements_list in checkboxes_dict.items():
        if key not in unwanted_elements:
            elements.update(elements_list.split(' '))
    return elements

def client_setup_add_image_names(server, setup_data):
    r = server.get('/images')

    images = r.json()['images']

    for disk, partitions in setup_data.items():
        for p in partitions:
            if images and p['image'] != 0:
                image = next((img for img in images if img['id'] == p['image']), None)
                if image:
                    p['image'] = image['name']
                else:
                    p['image'] = ""
            else:
                p['image'] = ""

def get_client_setup(ip):
    server = get_server_from_clients([ip])

    payload = {'clients': [ip]}
    r = server.post('/refresh', payload)

    payload = {'client': [ip]}
    r = server.get('/client/setup', payload)

    db_partitions = r.json()['partitions']
    res = {}
    for partition in db_partitions:
        if partition['partition'] == 0:
            partition['code'] = PART_SCHEME_CODES.get(partition['code'], 'MSDOS')
        else:
            partition['code'] = PART_TYPE_CODES.get(partition['code'], 'EMPTY')

        partition['filesystem'] = FS_CODES.get(partition['filesystem'], 'EMPTY')

        disk = partition.get('disk')
        if disk in res:
            res[disk].append(partition)
        else:
            res[disk] = [partition]

    return res

@app.route('/client/setup', methods=['GET'])
@login_required
def get_client_setup_json():
    ip = parse_elements(request.args.to_dict())
    setup = get_client_setup(ip)
    return jsonify(setup)

def get_clients(state_filter=None):
    responses = multi_request('get', '/clients')
    clients_list = []
    for r in responses:
        req_clients = r['json']['clients']
        for client in req_clients:
            client['server'] = r['server']
        clients_list = clients_list + req_clients

    clients = {}
    clients['clients'] = clients_list

    if state_filter:
        return filter(clients.items(), lambda c: c.state == state_filter)

    return clients


def get_repository(repository_id, server):
    repositories = get_repositories(server)
    [repository] = [repository for repository in repositories
                    if repository['id'] == repository_id]
    return repository


def get_repositories(server):
    r = server.get('/repositories')
    repositories = r.json()['repositories']
    return repositories

def get_all_repositories():
    data = multi_request('get', '/repositories')
    for item in data:
        repositories = item['json']['repositories']
        sorted_repositories = sorted(repositories, key=lambda repo: repo['name'])
        item['json']['repositories'] = sorted_repositories
    return data


def parse_scopes_from_tree(tree, scope_type, tree_path=''):
    scopes = []
    for scope in tree['scope']:
        if scope['type'] == scope_type:
            if 'name' in tree:
                scope['parent'] = tree['name']
            scope['tree_path'] = tree_path
            scopes.append(scope)
        else:
            new_tree_path = f'{tree_path}{scope.get("name")}/'
            scopes += parse_scopes_from_tree(scope, scope_type, new_tree_path)
    return scopes

def add_state_and_ips(scope, clients, ips):
    scope['selected'] = False
    if 'ip' in scope:
        filtered_client = filter(lambda x: x['addr']==scope['ip'], clients)
        client = next(filtered_client, False)
        if client:
            scope['state'] = client['state']
            scope['link'] = client.get('speed')
            scope['last_cmd'] = {}
            scope['last_cmd']['result'] = client.get('last_cmd').get('result')
        else:
            scope['state'] = 'off'
        scope['ip'] = [scope['ip']]
        scope['selected'] = set(scope['ip']).issubset(ips)
    else:
        scope['ip'] = []
        for child in scope['scope']:
            scope['ip'] += add_state_and_ips(child, clients, ips)
            scope['selected'] = (len(scope['ip']) < 0 and
                                 set(scope['ip']).issubset(ips))
    return scope['ip']

def remove_disabled_scopes(scopes):
    scope_list = scopes.get('scope')[:]
    for scope in scope_list:
        if scope.get('type') == 'center':
            if str(scope.get('id')) in current_user.scopes:
                continue

            scopes.get('scope').remove(scope)
        else:
            remove_disabled_scopes(scope)

def multi_request(method, uri, payload=None):
    responses = []

    for server in servers:
        response = {}

        if method == 'get':
            r = server.get(uri, payload)
        elif method == 'post':
            r = server.post(uri, payload)
        else:
            raise Exception('Invalid method, use get or post')

        response['server'] = server
        response['json'] = r.json()
        responses.append(response)

    return responses


def get_server_from_clients(selected_clients):
    server = None

    responses = multi_request('get', '/scopes')
    for r in responses:
        server_clients = [c['ip'] for c in parse_scopes_from_tree(r['json'],
                                                                  'computer')]
        if all(client in server_clients for client in selected_clients):
            server = r['server']
            break

    if not server:
        raise Exception('Selected clients not found in any server')

    return server


def get_server_from_ip_port(str_ip_port):
    ip, port = str_ip_port.split(':')

    for s in servers:
        if s.ip == ip and s.port == int(port):
            return s

    raise Exception('Server with address ' + str_ip_port + 'is not configured')

def sort_scopes_recursive(ls_scopes):
    for scope in ls_scopes:
        if len(scope['scope']) != 0:
            sorted_ls = sort_scopes_recursive(scope['scope'])
            scope['scope'] = sorted_ls
    return sorted(ls_scopes, key=lambda s: s['name'].lower())

def sort_scopes(scopes):
    all_scopes={}
    ls = sort_scopes_recursive(scopes['scope'])
    all_scopes['scope'] = ls
    return all_scopes

def get_scopes(ips=set()):
    list_scopes = []
    responses = multi_request('get', '/scopes')
    for r in responses:
        scopes = r['json']
        server_scope = {}
        server_scope['name'] = r['server'].name
        server_scope['type'] = "server"
        server_scope['server_ip_port'] = (r['server'].ip + ":" +
                                          str(r['server'].port))
        server_scope.update(scopes)
        list_scopes.append(server_scope)
    all_scopes = {'scope': list_scopes}
    all_scopes = sort_scopes(all_scopes)
    if not current_user.admin and current_user.scopes:
        remove_disabled_scopes(all_scopes)
    clients = get_clients()
    add_state_and_ips(all_scopes, clients['clients'], ips)

    return all_scopes, clients


def hash_password(pwd):
    sha = hashlib.sha512()
    sha.update(pwd.encode())
    pwd_hash = sha.hexdigest()

    return pwd_hash

def get_cache_info(clients_info, storage_data, images_data, client_images):
    for client_info in clients_info:
        ip = client_info['ip']
        cache_size = client_info['cache_size'] * 1024
        used_cache = client_info['used_cache']
        free_cache = client_info['free_cache']
        for image_info in client_info['images']:
            image_name = image_info['name']
            checksum = image_info['checksum']
            img_identifier = f'{image_name}.{checksum}'

            if ip in client_images:
                client_images[ip].append(img_identifier)
            else:
                client_images[ip] = [img_identifier]

            if img_identifier in images_data:
                images_data[img_identifier]['clients'].append(ip)
            else:
                images_data[img_identifier] = {
                    'clients': [ip],
                    'size': int(image_info['size']),
                    'name': image_name,
                    'checksum': checksum
                }

        storage_data[ip] = {'used': used_cache,
                            'free': free_cache}

def authenticate_user(username, pwd):
    for user in app.config['USERS']:
        if user.get("USER") == username:
            if user.get("PASS") == pwd:
                return user
            else:
                flash(_('Incorrect password'))
                return None
    flash(_('Incorrect user name'))
    return None

def get_user(username):
    for user in app.config['USERS']:
        if user.get("USER") == username:
            return user
    return None


intervals = (
    (_l('days'), 86400),    # 60 * 60 * 24
    (_l('hours'), 3600),    # 60 * 60
    (_l('minutes'), 60),
)


def display_time(seconds):
    result = []

    for name, count in intervals:
        value = seconds // count
        if value:
            seconds -= value * count
            result.append("{} {}".format(value, name))
    return ', '.join(result)


@login_manager.user_loader
def load_user(username):
    user_dict = get_user(username)
    if not user_dict:
        return None

    user = User(username,
                user_dict.get('SCOPES'),
                user_dict.get('ADMIN'),
                user_dict.get('PERMISSIONS', {}))
    return user

@app.errorhandler(404)
def page_not_found(error):
    return render_template('error.html', message=error), 404

@app.errorhandler(500)
def server_error(error):
    return render_template('error.html', message=error), 500

def image_modified_date_from_str(image):
    return datetime.datetime.strptime(image['modified'], '%a %b %d %H:%M:%S %Y')

def get_dashboard_data():
    dashboard_servers = {}
    images_response = multi_request('get', '/images')
    for i in images_response:
        server_name = i['server'].name
        server_id = i['server'].id
        images = i['json']['images']
        images.sort(key=image_modified_date_from_str, reverse=True)
        disk = i['json']['disk']

        if server_name not in dashboard_servers:
            dashboard_servers[server_id] = {'name': server_name}

        dashboard_servers[server_id]['images'] = images
        dashboard_servers[server_id]['disk'] = disk

    oglive_list = multi_request('get', '/oglive/list')
    for i in oglive_list:
        server_id = i['server'].id
        dashboard_servers[server_id]['oglive_list'] = i['json']

    all_stats = multi_request('get', '/stats')
    for server in servers:
        active = False
        stat = None
        for i in all_stats:
            if i['server'].id == server.id:
                active = True
                stat = i
                break
        if active:
            server_id = stat['server'].id
            dashboard_servers[server_id]['online'] = True
            stats = stat['json']
            dashboard_servers[server_id]['stats'] = stats
            timestamp = datetime.datetime.fromtimestamp(stats.get('time').get('now'))
            now = timestamp.strftime('%Y-%m-%d  %H:%M:%S')
            boot = display_time(stats.get('time').get('boot'))
            start = display_time(stats.get('time').get('start'))
            time_dict = {'now': now, 'boot': boot, 'start': start}
            dashboard_servers[server_id]['time_dict'] = time_dict
        else:
            dashboard_servers[server.id] = {}
            dashboard_servers[server.id]['online'] = False
            dashboard_servers[server.id]['name'] = server.name
            dashboard_servers[server.id]['time_dict'] = {'now': None, 'boot': 'Offline', 'start': None}
            dashboard_servers[server.id]['clients'] = []
            dashboard_servers[server.id]['images'] = []
            dashboard_servers[server.id]['disk'] = {'total': 0, 'free':0}
            dashboard_servers[server.id]['stats'] = {'memory': {'free': 0, 'size':0}, 'swap': ''}
            dashboard_servers[server.id]['oglive_list'] = []

    clients_response = multi_request('get', '/clients')
    for i in clients_response:
        server_id = i['server'].id
        dashboard_servers[server_id]['clients'] = i['json']['clients']
    return dashboard_servers

@app.route('/')
def index():
    if not current_user.is_authenticated:
        return redirect(url_for('login'))

    try:
        dashboard_servers = get_dashboard_data()
    except ServerError:
        dashboard_servers = {}
    timestamp = datetime.datetime.today()
    now = timestamp.strftime('%Y-%m-%d  %H:%M:%S')
    return render_template('dashboard.html', servers=dashboard_servers, now=now)

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm(request.form)
    if request.method == 'POST' and form.validate():
        form_user = request.form['user']
        pwd = request.form['pwd']
        pwd_hash = hash_password(pwd)
        user_dict = authenticate_user(form_user, pwd_hash)
        if not user_dict:
            return render_template('auth/login.html', form=form)
        user = User(form_user,
                    user_dict.get('SCOPES'),
                    user_dict.get('ADMIN'),
                    user_dict.get('PERMISSIONS', {}))
        login_user(user)
        return redirect(url_for('index'))
    return render_template('auth/login.html', form=LoginForm())

@app.route("/logout")
@login_required
def logout():
    logout_user()
    return redirect(url_for('index'))

@app.route('/scopes/status')
@login_required
def scopes_status():
    try:
        scopes, _clients = get_scopes()
    except ServerError:
        scopes = {}
    return jsonify(scopes)


@app.route('/client/mac', methods=['GET'])
@login_required
def get_client_mac():
    ip = parse_elements(request.args.to_dict())
    payload = {'client': list(ip)}
    server = get_server_from_clients(list(ip))
    try:
        resp = server.get('/client/info', payload)
    except ServerError:
        return {}
    client_info = resp.json()
    mac = client_info.get('mac')
    pretty_mac = prettify_mac(mac)
    return jsonify(pretty_mac)


def get_server_data_from_scopes(scopes, clients):
    servers_data = {}

    ip_to_name = {ip: c['name'] for c in parse_scopes_from_tree(scopes, 'computer') for ip in c['ip']}

    for client in clients['clients']:
        if not client['addr'] in ip_to_name:
            continue

        client['name'] = ip_to_name[client['addr']]
        server_id = client['server'].id
        if server_id not in servers_data:
            servers_data[server_id] = {'clients': []}

            for server in servers:
                if server.id == server_id:
                    servers_data[server_id]['name'] = server.name

        servers_data[server_id]['clients'].append(client)

    for server_id in servers_data:
        servers_data[server_id]['clients'].sort(key=lambda x: x['name'])

    return servers_data

@app.route('/client/list', methods=['GET'])
@handle_server_errors('index')
@login_required
def client_list():
    scopes, clients = get_scopes()
    servers_data = get_server_data_from_scopes(scopes, clients)

    selected_clients = list(get_selected_clients(scopes['scope']).items())

    return render_template('client_list.html', servers_data=servers_data,
                           selected_clients=selected_clients,
                           scopes=scopes)


@app.route('/scopes/')
@login_required
def scopes():
    try:
        scopes, clients = get_scopes()
    except ServerError:
        scopes = {}
        clients = {}
    return render_template('scopes.html', scopes=scopes, clients=clients)

@app.route('/action/poweroff', methods=['GET', 'POST'])
@handle_server_errors('commands')
@login_required
def action_poweroff():
    form = GenericForm(request.form)
    if request.method == 'POST':
        ips = form.ips.data.split(' ')
        if not validate_elements(ips):
            return redirect(url_for('commands'))

        payload = {'clients': ips}
        server = get_server_from_clients(ips)
        r = server.post('/poweroff', payload)

        flash(_('Client powered off successfully'), category='info')
        return redirect(url_for('commands'))
    else:
        ips = parse_elements(request.args.to_dict())
        form.ips.data = " ".join(ips)
        if validate_elements(ips):
            scopes, clients = get_scopes(set(ips))
            selected_clients = list(get_selected_clients(scopes['scope']).items())
            return render_template('actions/poweroff.html', form=form,
                                   selected_clients=selected_clients,
                                   scopes=scopes)
        else:
            return redirect(url_for('commands'))


@app.route('/action/wol', methods=['GET', 'POST'])
@handle_server_errors('commands')
@login_required
def action_wol():
    form = WOLForm(request.form)
    if request.method == 'POST' and form.validate():
        wol_type = form.wol_type.data
        ips = form.ips.data.split(' ')
        payload = {'type': wol_type, 'clients': ips}
        server = get_server_from_clients(ips)
        server.post('/wol', payload)
        flash(_('Wake On Lan request sent successfully'), category='info')
        return redirect(url_for('commands'))
    else:
        ips = parse_elements(request.args.to_dict())
        form.ips.data = " ".join(ips)
        if validate_elements(ips, min_len=1):
            scopes, clients = get_scopes(set(ips))
            selected_clients = list(get_selected_clients(scopes['scope']).items())
            return render_template('actions/wol.html', form=form,
                                   selected_clients=selected_clients,
                                   scopes=scopes)
        else:
            return redirect(url_for('commands'))


def get_common_disk_data(ips):
    disk_data = {}

    for ip in ips:
        setup_data = get_client_setup(ip)

        for disk, partitions in setup_data.items():
            for p in partitions:
                if p.get('partition') != 0:
                    continue
                if disk in disk_data:
                    disk_data[disk]['common_size'] = min(disk_data[disk]['common_size'], p.get('size'))
                    disk_data[disk]['inventory'].setdefault(p.get('size'), []).append(ip)
                else:
                    disk_data[disk] = {
                        'common_size': p.get('size'),
                        'excluded': [],
                        'inventory': {p.get('size'): [ip]}
                    }
                break

    for disk_id in disk_data:
        disk_ips = []
        for disk_size in disk_data[disk_id]['inventory']:
            disk_ips.extend(disk_data[disk_id]['inventory'][disk_size])

        disk_data[disk_id]['excluded'] = [ip for ip in ips if ip not in disk_ips]

    return disk_data

@app.route('/action/setup/select', methods=['GET'])
@handle_server_errors('commands')
@login_required
def action_setup_select():
    args = request.args.copy()

    ips = parse_elements(args.to_dict())
    if not validate_elements(ips):
        return redirect(url_for('commands'))

    if len(ips) == 1:
        ip = list(ips)[0]
        return redirect(url_for('action_setup_show', ip=ip))

    form = SelectClientForm()
    form.ips.data = " ".join(ips)

    server = get_server_from_clients(list(ips))
    client_choices = []
    for ip in ips:
        r = server.get('/client/info', payload={'client': [ip]})

        client_name = r.json()['name']
        client_choices.append((ip, f"{client_name} ({ip})"))
    form.selected_client.choices = client_choices

    common_disk_data = get_common_disk_data(ips)

    scopes, unused = get_scopes(ips)
    selected_clients = list(get_selected_clients(scopes['scope']).items())

    return render_template('actions/select_client.html',
                           common_disk_data=common_disk_data,
                           selected_clients=selected_clients,
                           form=form, scopes=scopes)


@app.route('/action/setup', methods=['GET'])
@handle_server_errors('commands')
@login_required
def action_setup_show():
    args = request.args.copy()
    disk_size = None

    if args.get('ip'):
        ips = {args['ip']}
        ips_str = base_client = args['ip']
    else:
        ips_str = args['ips']
        ips = set(args['ips'].split(' '))
        base_client = args['selected_client']

    setup_data = get_client_setup(base_client)

    if not setup_data:
        flash(_('Partition information is not available. Boot client in ogLive mode to obtain it'), category='error')
        return redirect(url_for('commands'))

    selected_disk = 1
    common_disk_data = get_common_disk_data(ips)

    # Use common disk size
    for disk in setup_data:
        setup_data[disk][0]['size'] = common_disk_data[disk]['common_size']

    excluded_client_disks = {}
    for disk in common_disk_data:
        excluded_client_disks[disk] = common_disk_data[disk]['excluded']

    form = SetupForm()
    form.ips.data = ips_str

    form.disk.choices = [(disk, disk) for disk in setup_data]
    form.disk.data = selected_disk
    # If partition table is empty, set MSDOS
    form.disk_type.data = setup_data[selected_disk][0]['code'] or 1

    scopes, _clients = get_scopes(ips)
    return render_template('actions/setup.html',
                           excluded_client_disks=excluded_client_disks,
                           selected_disk=selected_disk,
                           setup_data=setup_data,
                           disk_form=form,
                           scopes=scopes)

@app.route('/action/setup', methods=['POST'])
@handle_server_errors('commands')
@login_required
def action_setup_modify():
    form = SetupForm(request.form)
    MIN_EFI_SIZE = 500
    if not form.validate():
        flash(_(f'Invalid setup form'), category='error')
        return redirect(url_for('commands'))

    cache_count = 0
    for partition in form.partitions:
        if partition.part_type.data == 'CACHE':
            cache_count += 1

        if partition.part_type.data == 'EFI' and partition.fs.data != 'FAT32':
            flash(_('The EFI partition requires a FAT32 filesystem'), category='error')
            return redirect(url_for('commands'))

        if partition.part_type.data == 'EFI' and partition.size.data < MIN_EFI_SIZE:
            flash(_(f'The EFI partition requires a size of {MIN_EFI_SIZE}MiB or higher'), category='error')
            return redirect(url_for('commands'))

        if partition.size.data <= 0:
            flash(_('Partitions can\'t have a size of zero or lower'), category='error')
            return redirect(url_for('commands'))

    if cache_count > 1:
        flash(_(f'More than one cache partition is not supported'), category='error')
        return redirect(url_for('commands'))

    ips = form.ips.data.split(' ')

    payload = {'clients': ips,
                'disk': str(form.disk.data),
                'type': str(form.disk_type.data),
                'cache': str(0),
                'cache_size': str(0),
                'partition_setup': []}

    partition_index = 0

    for partition in form.partitions:
        partition_index += 1
        partition_setup = {'partition': str(partition_index),
                            'code': str(partition.part_type.data),
                            'filesystem': str(partition.fs.data),
                            'size': str(partition.size.data * 1024),
                            'format': '1'}
        payload['partition_setup'].append(partition_setup)

        if partition.part_type.data == 'CACHE':
            payload['cache'] = '1'
            payload['cache_size'] = str(partition.size.data * 1024)

    for partition_index in range(len(form.partitions) + 1, 5):
        empty_part = {
            'partition': str(partition_index),
            'code': 'EMPTY',
            'filesystem': 'EMPTY',
            'size': '0',
            'format': '1',
        }
        payload['partition_setup'].append(empty_part)

    server = get_server_from_clients(list(ips))
    r = server.post('/setup', payload=payload)
    flash(_(f'Setup command sent sucessfully'), category='info')
    return redirect(url_for('commands'))

def search_image(images_list, image_id):
    for image in images_list:
        if image['id'] == image_id:
            return image
    return False

def filter_images_allowed_in_center(server, images, center_id):
    res = []
    for image in images:
        r = server.get('/image/restrict', {'image': image['id']})
        allowed_scopes = r.json().get('scopes')
        if not allowed_scopes or center_id in allowed_scopes:
            res.append(image)
    return res

def get_images_from_repo(server, repo_id):
    r = server.get('/images')
    images = r.json()['images']
    res=[]
    for image in images:
        if image['repo_id'] == repo_id:
            res.append(image)
    return res

def get_image_key(image):
    return image['name']

def sort_images(images):
    images.sort(key=get_image_key, reverse=False)

def get_clients_repo(server, ips):
    repo_id=None
    for ip in ips:
        r = server.get('/client/info', payload={'client': [ip]})
        repo_id_aux = r.json()['repo_id']
        if repo_id is None:
            repo_id = repo_id_aux
        elif repo_id_aux != repo_id:
            return None
    return repo_id

def image_fits_in_cache(server, clients_info, image):
    image_size = int(image.get('size', '0'))
    image_checksum = image.get('checksum', '')

    if not (image_size and image_checksum):
        return True

    err_report = ""

    for client_info in clients_info:
        ip = client_info['ip']
        cache_size = client_info['cache_size'] * 1024
        has_image = False;

        for image_info in client_info['images']:
            if image_info['checksum'] == image_checksum:
                has_image = True

        if has_image:
            continue

        free_cache = client_info['free_cache']
        used_cache = client_info['used_cache']
        if used_cache == 0 and free_cache == 0:
            err_report += f'{ip} has no cache partition<br>'
            continue

        if free_cache < image_size:
            missing_cache = image_size - free_cache
            err_report += f'{ip} requires {(missing_cache / (1024 ** 3)):.3f} more free GiB<br>'

    if err_report:
        flash(f'{err_report}', category='error')
        return False

    return True


class PartitionCollection:
    def __init__(self):
        self.partition_list = []
        self.clients = []
        self.empty_scheme = False

    def __len__(self):
        return len(self.partition_list)

    def has_empty_scheme(self):
        return self.empty_scheme

    def register_partition_setup(self, partitions, client):
        idx = None
        self.empty_scheme = self.empty_scheme or not partitions
        for index, p in enumerate(self.partition_list):
            if p == partitions:
                idx = index
                self.clients[idx].append(client)
                break
        else:
            idx = len(self.partition_list)
            self.partition_list.append(partitions)
            self.clients.append([client])

        return idx

    def get_partition_setup(self, idx):
        return self.partition_list[idx]

    def get_clients(self, idx):
        return self.clients[idx]

@app.route('/action/image/restore', methods=['GET', 'POST'])
@handle_server_errors('commands')
@login_required
def action_image_restore():
    form = ImageRestoreForm(request.form)
    if request.method == 'POST':
        ips = form.ips.data.split(' ')
        disk, partition, part_size, has_cache = form.partition.data.split(' ')

        requires_cache = form.method.data == 'TIPTORRENT' or form.method.data == 'UNICAST'
        if has_cache == 'False' and requires_cache:
            flash(_(f'Cannot restore image using {form.method.data} on a client without cache'), category='error')
            return redirect(url_for('commands'))

        image_id = form.image.data
        server = get_server_from_clients(ips)
        r = server.get('/images')

        images_list = r.json()['images']
        image = search_image(images_list, int(image_id))
        if not image:
            flash(_(f'Image to restore was not found'), category='error')
            return redirect(url_for('commands'))

        image_datasize = int(image['datasize'])
        part_size = int(part_size) * 1024
        if image_datasize and image_datasize > part_size:
            flash(_(f'The image size is bigger than the target partition'), category='error')
            return redirect(url_for('commands'))

        r = server.get('/cache/list', payload={'clients': ips})

        clients_info = r.json()['clients']

        if requires_cache and not image_fits_in_cache(server, clients_info, image):
            return redirect(url_for('commands'))

        payload = {'disk': disk,
                   'partition': partition,
                   'name': image['name'],
                   'repository_id': image['repo_id'],
                   'clients': ips,
                   'type': form.method.data,
                   'profile': str(image['software_id']),
                   'id': str(image['id'])}
        r = server.post('/image/restore', payload)
        flash(_(f'Image restore command sent sucessfully'), category='info')
        return redirect(url_for('commands'))
    else:
        params = request.args.to_dict()
        ips = parse_elements(params)
        if not validate_elements(ips):
            return redirect(url_for('commands'))

        center_id = int(params.get('scope-center'))
        form.ips.data = ' '.join(ips)

        server = get_server_from_clients(ips)

        repo_id = get_clients_repo(server, ips)

        if repo_id is None:
            flash(_(f'Computers have different repos assigned'), category='error')
            return redirect(url_for('commands'))

        images = get_images_from_repo(server, repo_id)

        if not images:
            flash(_(f'Computer(s) assigned to a repo with no images'), category='error')
            return redirect(url_for('commands'))

        images = filter_images_allowed_in_center(server, images, center_id)

        sort_images(images)

        for image in images:
            form.image.choices.append((image['id'], image['name']))

        invalid_part_types = get_invalid_image_partition_types()

        part_collection = PartitionCollection()

        for ip in ips:
            r = server.get('/client/setup', payload={'client': [ip]})

            has_cache = False

            partitions = r.json()['partitions']
            parts = []
            for partition in partitions:
                disk_id = partition['disk']
                part_id = partition['partition']
                if part_id == 0:  # This is the disk data, not a partition.
                    continue
                if disk_id != 1:
                    continue
                part_code = partition['code']
                part_type = PART_TYPE_CODES.get(int(part_code), 'UNKNOWN')

                if part_type == 'CACHE':
                    has_cache = True

                if  part_type in invalid_part_types:
                    continue

                filesystem = partition['filesystem']
                fs_type = FS_CODES.get(filesystem, 'UNKNOWN')
                part_size = partition['size']

                parts.append((disk_id, part_id, part_type, fs_type, part_size))

            part_collection.register_partition_setup(parts, ip)

        scopes, clients = get_scopes(set(ips))
        selected_clients = list(get_selected_clients(scopes['scope']).items())

        if len(part_collection) > 1 or part_collection.has_empty_scheme():
            return render_template('actions/partition_report.html',
                               selected_clients=selected_clients,
                               part_data=part_collection,
                               scopes=scopes)

        reference_patitioning = part_collection.get_partition_setup(0)

        if not reference_patitioning:
            flash(_(f'No valid partition found'), category='error')
            return redirect(url_for('commands'))

        for disk_id, part_id, part_type, fs_type, part_size in reference_patitioning:
            form.partition.choices.append(
                (f"{disk_id} {part_id} {part_size} {has_cache}",
                f"Disk {disk_id} | Partition {part_id} "
                f"| {part_type} "
                f"{fs_type}")
            )

        return render_template('actions/image_restore.html', form=form,
                               selected_clients=selected_clients,
                               scopes=scopes)

@app.route('/action/hardware', methods=['GET', 'POST'])
@handle_server_errors('commands')
@login_required
def action_hardware():
    form = HardwareForm(request.form)
    if request.method == 'POST':
        ips = form.ips.data.split(' ')
        server = get_server_from_clients(ips)
        r = server.post('/hardware', payload={'clients': ips})
        flash(_(f'Hardware inventory command sent sucessfully'), category='info')
        return redirect(url_for('commands'))
    else:
        ips = parse_elements(request.args.to_dict())
        if not validate_elements(ips, max_len=1):
            return redirect(url_for('commands'))

        scopes, _clients = get_scopes(ips)
        form.ips.data = ' '.join(ips)
        server = get_server_from_clients(ips)
        r = server.get('/hardware', payload={'client': list(ips)})

        hardware = r.json()['hardware']
        return render_template('actions/hardware.html', form=form,
                               hardware=hardware, scopes=scopes)

@app.route('/action/software', methods=['GET', 'POST'])
@handle_server_errors('commands')
@login_required
def action_software():
    form = SoftwareForm(request.form)
    if request.method == 'POST':
        ips = form.ips.data.split(' ')
        disk, partition = form.os.data.split(' ')
        server = get_server_from_clients(ips)
        if form.view.data:
            r = server.get('/software', payload={'client': ips,
                            'disk': int(disk),
                            'partition': int(partition)})

            software = r.json()['software']
            scopes, clients = get_scopes(set(ips))
            return render_template('actions/software_list.html',
                                   software=software, form=form, scopes=scopes)
        elif form.update.data:
            r = server.post('/software', payload={'clients': ips,
                                                    'disk': disk,
                                                    'partition': partition})
            flash(_('Software profile request sent successfully'), category='info')
        else:
            flash(_('Error processing software profile form'), category='error')
        return redirect(url_for('commands'))
    else:
        ips = parse_elements(request.args.to_dict())
        if not validate_elements(ips, max_len=1):
            return redirect(url_for('commands'))

        scopes, clients = get_scopes(set(ips))
        form.ips.data = ' '.join(ips)
        server = get_server_from_clients(ips)
        r = server.get('/client/setup', payload={'client': list(ips)})

        if not r.json()['partitions']:
            flash(_('Software inventory is not available. Boot client in ogLive mode to obtain it'), category='error')
            return redirect(url_for('commands'))

        for part in r.json()['partitions'][1:]:
            form.os.choices.append(
                (f"{part.get('disk')} {part.get('partition')}",
                 f"Disk {part.get('disk')} | Partition {part.get('partition')} "
                 f"| {PART_TYPE_CODES.get(part.get('code'), 'UNKNOWN')} "
                 f"{FS_CODES.get(part.get('filesystem'), 'UNKNOWN')}")
            )
    return render_template('actions/software.html', form=form, scopes=scopes)

@app.route('/action/session', methods=['GET', 'POST'])
@handle_server_errors('commands')
@login_required
def action_session():
    form = SessionForm(request.form)
    if request.method == 'POST':
        ips = form.ips.data.split(' ')
        disk, partition, os_name = form.os.data.split(' ', 2)
        server = get_server_from_clients(list(ips))

        r = server.get('/session', payload={'clients': ips})

        sessions = r.json()['sessions']
        if not sessions:
            flash(_('ogServer returned an empty session list'),
            category='error')
            return redirect(url_for('commands'))

        valid_ips = []
        excluded_ips = []
        for client_sessions in sessions:
            ip = client_sessions['addr']
            os_found = False
            for p_data in client_sessions['partitions']:
                if p_data['disk'] == int(disk) and p_data['partition'] == int(partition) and p_data['name'] == os_name:
                    os_found = True
                    break
            if os_found:
                valid_ips.append(ip)
            else:
                excluded_ips.append(ip)

        r = server.post('/session', payload={'clients': valid_ips,
                                             'disk': str(disk),
                                             'partition': str(partition)})
        if excluded_ips:
            flash('The following clients didn\'t match the boot configuration: ' + str(excluded_ips))
            return redirect(url_for('commands'))

        flash(_(f'Boot OS command sent sucessfully'), category='info')
        return redirect(url_for('commands'))
    else:
        ips = parse_elements(request.args.to_dict())
        ips_list = list(ips)
        if not validate_elements(ips):
            return redirect(url_for('commands'))

        server = get_server_from_clients(ips_list)
        form.ips.data = ' '.join(ips_list)

        r = server.get('/session', payload={'clients': ips_list})

        sessions = r.json()['sessions']
        if not sessions:
            flash(_('ogServer returned an empty session list'),
            category='error')
            return redirect(url_for('commands'))

        os_groups = {}
        for client_sessions in sessions:
            ip = client_sessions['addr']
            for p_data in client_sessions['partitions']:
                if p_data['name'] == 'unknown':
                    continue

                item_key = f"{p_data['disk']} {p_data['partition']} {p_data['name']}"

                if item_key in os_groups:
                    os_groups[item_key].append(ip)
                else:
                    os_groups[item_key] = [ip]

                    choice = (item_key,
                        f"{p_data['name']} (Disk:{p_data['disk']}, Partition:{p_data['partition']})")
                    form.os.choices.append(choice)

        scopes, clients = get_scopes(set(ips))
        selected_clients = list(get_selected_clients(scopes['scope']).items())
        return render_template('actions/session.html', form=form,
                               selected_clients=selected_clients,
                               scopes=scopes, os_groups=os_groups)

@app.route('/action/cache', methods=['GET', 'POST'])
@handle_server_errors('commands')
@login_required
def action_client_cache():
    form = CacheForm(request.form)
    if request.method == 'POST':
        ips = form.ips.data.split(' ')
        server = get_server_from_clients(list(ips))

        client_list = []
        image_list = []
        for entry in form.images.entries:
                if not entry.selected.data:
                    continue

                image_list.append(entry.image_name.data)
                for client_ip in entry.clients.data.split(' '):
                    if not client_ip in client_list:
                        client_list.append(client_ip)

        if not image_list:
            flash(_(f'No selected images to delete'), category='error')
            return redirect(url_for('commands'))

        r = server.post('/cache/delete',
                        payload={'clients': client_list,
                                 'images': image_list})
        flash(_('Cache delete request sent successfully'), category='info')
        return redirect(url_for('commands'))
    else:
        ips = parse_elements(request.args.to_dict())
        ips_list = list(ips)
        if not validate_elements(ips):
            return redirect(url_for('commands'))

        form.ips.data = ' '.join(ips_list)

        server = get_server_from_clients(ips_list)
        r = server.get('/cache/list', payload={'clients': ips_list})

        clients_info = r.json()['clients']

        if not clients_info:
            flash(_('ogServer returned an empty client list'), category='error')
            return redirect(url_for('commands'))

        storage_data = {}
        images_data = {}
        client_images = {}

        get_cache_info(clients_info, storage_data, images_data, client_images)

        for img_identifier in images_data:
                image_data = images_data[img_identifier]
                checkbox_entry = form.images.append_entry()
                checkbox_entry.selected.label.text = img_identifier
                checkbox_entry.image_name.data = image_data['name']
                checkbox_entry.clients.data = ' '.join(image_data['clients'])

        scopes, clients = get_scopes(set(ips))
        selected_clients = list(get_selected_clients(scopes['scope']).items())
        return render_template('actions/cache.html', form=form,
                               selected_clients=selected_clients,
                               scopes=scopes, images_data=images_data,
                               storage_data=storage_data,
                               client_images=client_images)

@app.route('/action/cache/fetch', methods=['GET', 'POST'])
@handle_server_errors('commands')
@login_required
def action_image_fetch():
    form = ImageFetchForm(request.form)
    if request.method == 'POST':
        ips = form.ips.data.split(' ')
        server = get_server_from_clients(list(ips))

        image_id = form.image.data
        server = get_server_from_clients(ips)
        r = server.get('/images')

        images_list = r.json()['images']
        image = search_image(images_list, int(image_id))
        if not image:
            flash(_(f'Image to fetch was not found'), category='error')
            return redirect(url_for('commands'))

        r = server.get('/cache/list', payload={'clients': ips})

        clients_info = r.json()['clients']

        if not image_fits_in_cache(server, clients_info, image):
            return redirect(url_for('commands'))

        payload = {'clients': ips,
                   'repository_id': image['repo_id'],
                   'type': form.method.data,
                   'image': image['name']}

        r = server.post('/cache/fetch', payload=payload)
        flash(_('Cache fetch request sent successfully'), category='info')
        return redirect(url_for('commands'))
    else:
        params = request.args.to_dict()
        ips = parse_elements(params)

        if not validate_elements(ips):
            return redirect(url_for('commands'))

        center_id = int(params.get('scope-center'))
        ips_list = list(ips)
        form.ips.data = ' '.join(ips_list)

        server = get_server_from_clients(ips)

        repo_id = get_clients_repo(server, ips)

        if repo_id is None:
            flash(_(f'Computers have different repos assigned'), category='error')
            return redirect(url_for('commands'))
        images = get_images_from_repo(server, repo_id)

        if not images:
            flash(_(f'Computer(s) assigned to a repo with no images'), category='error')
            return redirect(url_for('commands'))

        images = filter_images_allowed_in_center(server, images, center_id)

        sort_images(images)

        for image in images:
            form.image.choices.append((image['id'], image['name']))

        scopes, clients = get_scopes(set(ips))
        selected_clients = list(get_selected_clients(scopes['scope']).items())
        return render_template('actions/cache_fetch.html', form=form,
                               selected_clients=selected_clients,
                               scopes=scopes)

@app.route('/action/client/info', methods=['GET'])
@handle_server_errors('commands')
@login_required
def action_client_info():
    form = ClientDetailsForm()
    ips = parse_elements(request.args.to_dict())
    if not validate_elements(ips, max_len=1):
        return redirect(url_for('commands'))

    server = get_server_from_clients(list(ips))

    payload = {'client': list(ips)}
    r = server.get('/client/info', payload)

    db_client = r.json()

    form.name.data = db_client['name']
    form.name.render_kw = {'readonly': True}
    form.ip.data = db_client['ip']
    form.ip.render_kw = {'readonly': True}
    form.mac.data = prettify_mac(db_client['mac'])
    form.mac.render_kw = {'readonly': True}
    form.serial_number.data = db_client['serial_number']
    form.serial_number.render_kw = {'readonly': True}
    form.livedir.choices = [(db_client['livedir'], db_client['livedir'])]
    form.livedir.render_kw = {'readonly': True}
    form.remote.data = db_client['remote']
    form.remote.render_kw = {'readonly': True}
    form.maintenance.data = db_client['maintenance']
    form.maintenance.render_kw = {'readonly': True}
    repositories = get_repositories(server)
    form.repo.choices = [(repo["id"], repo["name"]) for repo in repositories
                         if db_client['repo_id'] == repo["id"]]
    form.repo.render_kw = {'readonly': True}
    form.room.data = db_client['room']
    form.room.render_kw = {'readonly': True}
    form.boot.choices = [(db_client['boot'], db_client['boot'])]
    form.boot.render_kw = {'readonly': True}

    r = server.get('/scopes')

    rooms = parse_scopes_from_tree(r.json(), 'room')
    rooms = [(room['id'], room['name']) for room in rooms
             if room['id'] == int(db_client['room'])]
    form.room.choices = list(rooms)

    form.submit.render_kw = {"style": "visibility:hidden;"}

    ip = list(ips)[0]
    setup_data = get_client_setup(ip)
    client_setup_add_image_names(server, setup_data)

    disk_form = SetupForm()
    selected_disk = 1
    disk_form.disk.choices = [(disk, disk) for disk in setup_data]
    disk_form.disk.data = selected_disk

    r = server.get('/cache/list', payload={'clients': [ip]})

    clients_info = r.json()['clients']

    if not clients_info:
        flash(_('ogServer returned an empty client list'), category='error')
        return redirect(url_for('commands'))

    storage_data = {}
    images_data = {}
    client_images = {}

    get_cache_info(clients_info, storage_data, images_data, client_images)

    r = server.get('/efi', payload={'clients': [ip]})

    efi_data = r.json()['clients'][0]

    scopes, clients = get_scopes(set(ips))

    return render_template('actions/client_details.html', form=form,
                           parent="commands.html", scopes=scopes,
                           setup_data=setup_data,
                           disk_form=disk_form,
                           selected_disk=selected_disk,
                           images_data=images_data,
                           storage_data=storage_data,
                           client_images=client_images,
                           efi_data=efi_data)

@app.route('/action/client/update', methods=['GET', 'POST'])
@handle_server_errors('scopes')
@login_required
def action_client_update():
    form = ClientDetailsForm(request.form)
    if request.method == 'POST':

        if not is_valid_ip(form.ip.data):
            flash(_('Invalid IP address'), category='error')
            return redirect(url_for("scopes"))

        mac_address = normalize_mac(form.mac.data)

        if not is_valid_normalized_mac(mac_address):
            flash(_('Invalid MAC address'), category='error')
            return redirect(url_for("scopes"))

        payload = {"ip": form.ip.data,
                   "id": int(form.client_id.data),
                   "serial_number": form.serial_number.data,
                   "netdriver": "generic",
                   "maintenance": form.maintenance.data,
                   "netiface": "eth0",
                   "repo_id": int(form.repo.data),
                   "netmask": "0",
                   "remote": form.remote.data,
                   "room": int(form.room.data),
                   "name": form.name.data,
                   "boot": form.boot.data,
                   "mac": mac_address }
        server = get_server_from_ip_port(form.server.data)
        r = server.post('/client/update', payload)

        flash(_('Client updated successfully'), category='info')
        return redirect(url_for("scopes"))
    else:
        ips = parse_elements(request.args.to_dict())
        if not validate_elements(ips, max_len=1):
            return redirect(url_for('scopes'))

        server = get_server_from_clients(list(ips))
        scopes, clients = get_scopes()

        payload = {'client': list(ips)}
        r = server.get('/client/info', payload)

        db_client = r.json()

        form.server.data = "{0}:{1}".format(server.ip, server.port)
        form.client_id.data = db_client['id']
        form.ip.data = db_client['ip']
        form.name.data = db_client['name']
        form.mac.data = prettify_mac(db_client['mac'])
        form.serial_number.data = db_client['serial_number']
        form.livedir.render_kw = {'readonly': True}
        form.remote.data = db_client['remote']
        form.maintenance.data = db_client['maintenance']

        current_mode = db_client['boot']
        r = server.get('/mode')

        available_modes = [(current_mode, current_mode)]
        available_modes.extend([(mode, mode) for mode in r.json()['modes']
                                if mode != current_mode])
        form.boot.choices = list(available_modes)
        form.boot.render_kw = {'readonly': True}

        r = server.get('/scopes')

        room_id = db_client['room']
        rooms = parse_scopes_from_tree(r.json(), 'room')
        rooms = [(room['id'], room['name']) for room in rooms
                 if room['id'] == int(room_id)]
        form.room.choices = list(rooms)
        form.room.render_kw = {'readonly': True}

        repositories = get_repositories(server)
        form.repo.choices = [(repo["id"], repo["name"]) for repo in repositories
                             if db_client['repo_id'] == repo["id"]]
        form.repo.choices.extend([(repo["id"], repo["name"]) for repo in repositories
                             if db_client['repo_id'] != repo["id"]])
        ip = list(ips)[0]
        setup_data = get_client_setup(ip)
        client_setup_add_image_names(server, setup_data)

        disk_form = SetupForm()
        selected_disk = 1
        disk_form.disk.choices = [(disk, disk) for disk in setup_data]
        disk_form.disk.data = selected_disk

        form.submit.render_kw = {"formaction": url_for('action_client_update')}

        return render_template('actions/client_details.html', form=form,
                               setup_data=setup_data,
                               disk_form=disk_form,
                               selected_disk=selected_disk,
                               parent="scopes.html", scopes=scopes)

def find_folder(folder_id, scopes):
    scopes = deque([scopes['scope']])
    while scopes:
        scope_ls = scopes.popleft()
        if not scope_ls:
            continue
        else:
            for scope in scope_ls:
                if scope['type'] == 'folder' and scope['id'] == folder_id:
                    return scope
                else:
                    scopes.append(scope['scope'])
    return None

@app.route('/action/folder/delete', methods=['GET', 'POST'])
@handle_server_errors('scopes')
@login_required
def action_folder_delete():
    form = FolderForm(request.form)
    if request.method == 'POST':
        payload = {"id": int(form.folder_id.data)}
        server = get_server_from_ip_port(form.server.data)
        r = server.post('/folder/delete', payload)
        flash(_('Folder deleted successfully'), category='info')
        return redirect(url_for("scopes"))
    else:
        params = request.args.to_dict()
        folder_id = params.get('folder')
        if not folder_id:
            flash(_('Please, select a folder'), category='error')
            return redirect(url_for('scopes'))
        form.folder_id.data = folder_id
        form.server.data = params['scope-server']
        form.submit.render_kw = {"formaction": url_for('action_folder_delete')}

        scopes, unused = get_scopes()

        ancestors, children = get_scope_context(int(folder_id), 'folder', scopes)
        del form.name

        return render_template('actions/folder_delete.html', form=form,
                               parent="scopes.html", scopes=scopes, ancestors=ancestors, children=children)

@app.route('/action/folder/update', methods=['GET','POST'])
@handle_server_errors('scopes')
@login_required
def action_folder_update():
    form = FolderForm(request.form)
    if request.method == 'POST':
        payload = {'name': form.name.data,
                   'id': int(form.folder_id.data)}

        server = get_server_from_ip_port(form.server.data)
        r = server.post('/folder/update', payload)
        flash(_('Folder updated successfully'), category='info')
        return redirect(url_for("scopes"))
    else:
        params = request.args.to_dict()

        if not 'folder' in params:
            flash(_('Please, select a folder to modify'), category='error')
            return redirect(url_for('scopes'))

        folder_id = int(params.get('folder'))
        scopes, clients = get_scopes()
        folder = find_element_scope(folder_id, 'folder', scopes)
        form.server.data = params['scope-server']
        form.name.data = folder['name']
        form.folder_id.data = folder_id
        form.submit.render_kw = {"formaction": url_for('action_folder_update')}
        return render_template('actions/folder_update.html', form=form,
                               parent="scopes.html", scopes=scopes)

@app.route('/action/folder/add', methods=['GET'])
@handle_server_errors('scopes')
@login_required
def action_folder_add():
    form = FolderForm()
    params = request.args.to_dict()
    room = params.get('scope-room')
    center = params.get('scope-center')
    if room and center:
        flash(_('Please, select either a room or a center'), category='error')
        return redirect(url_for('scopes'))
    if not room and not center:
        flash(_('Please, select a room or a center'), category='error')
        return redirect(url_for('scopes'))
    if params.get('folder'):
        flash(_('Error: A folder has been selected. Please, select a room or a center'), category='error')
        return redirect(url_for('scopes'))
    form.server.data = params['scope-server']
    form.room.data = room
    form.center.data = center
    form.submit.render_kw = {"formaction": url_for('action_folder_add_post')}
    scopes, unused = get_scopes()
    return render_template('actions/folder_add.html', form=form,
                           parent="scopes.html", scopes=scopes)

@app.route('/action/folder/add', methods=['POST'])
@handle_server_errors('scopes')
@login_required
def action_folder_add_post():
    form = FolderForm(request.form)
    payload = {"name": form.name.data}
    if form.center.data:
        payload["center"] = int(form.center.data)
    if form.room.data:
        payload["room"] = int(form.room.data)

    server = get_server_from_ip_port(form.server.data)
    r = server.post('/folder/add', payload)
    flash(_('Folder added successfully'), category='info')
    return redirect(url_for("scopes"))

@app.route('/action/client/add', methods=['GET', 'POST'])
@handle_server_errors('scopes')
@login_required
def action_client_add():
    form = ClientDetailsForm(request.form)
    if request.method == 'POST':

        if not form.validate():
            flash(form.errors, category='error')
            return redirect(url_for('scopes'))

        client_name = form.name.data

        if not client_name:
            flash(_('Invalid empty client name'), category='error')
            return redirect(url_for("scopes"))

        if not is_valid_ip(form.ip.data):
            flash(_('Invalid IP address'), category='error')
            return redirect(url_for("scopes"))

        mac_address = normalize_mac(form.mac.data)

        if not is_valid_normalized_mac(mac_address):
            flash(_('Invalid MAC address'), category='error')
            return redirect(url_for("scopes"))

        payload = {"boot": form.boot.data,
                   "ip": form.ip.data,
                   "livedir": form.livedir.data,
                   "mac": mac_address,
                   "maintenance": form.maintenance.data,
                   "name": form.name.data,
                   "netdriver": "generic",
                   "netiface": "eth0",
                   "netmask": "0",
                   "remote": form.remote.data,
                   "repo_id": int(form.repo.data),
                   "room": int(form.room.data),
                   "serial_number": form.serial_number.data,
                   "folder_id": int(form.folder_id.data) }

        server = get_server_from_ip_port(form.server.data)
        r = server.post('/client/add', payload)
        flash(_('Client added successfully'), category='info')
        return redirect(url_for("scopes"))
    else:
        params = request.args.to_dict()
        if not params.get('scope-room'):
            flash(_('Please, select a room or a folder'), category='error')
            return redirect(url_for('scopes'))
        form.server.data = params['scope-server']
        server = get_server_from_ip_port(params['scope-server'])
        r = server.get('/mode')

        available_modes = [(mode, mode) for mode in r.json()['modes'] if mode == 'pxe']
        available_modes.extend([(mode, mode) for mode in r.json()['modes'] if mode != 'pxe'])

        form.boot.choices = list(available_modes)
        form.mac.render_kw = {'placeholder': 'aa:bb:cc:dd:ee:aa'}

        r = server.get('/scopes')

        room_id = params['scope-room']
        rooms = parse_scopes_from_tree(r.json(), 'room')
        rooms = [(room['id'], room['name']) for room in rooms
                 if room['id'] == int(room_id)]
        form.room.choices = list(rooms)
        form.room.render_kw = {'readonly': True}

        repositories = get_repositories(server)
        form.repo.choices = [(repo["id"], repo["name"]) for repo in repositories]

        if params.get('folder'):
            form.folder_id.data = params['folder']
        else:
            form.folder_id.data = 0

        form.submit.render_kw = {"formaction": url_for('action_client_add')}
        scopes, clients = get_scopes()
        return render_template('actions/client_add.html', form=form,
                               parent="scopes.html", scopes=scopes)

def build_client_move_choices(scopes, choices, path):
    for scope in scopes.get('scope'):
        new_path = f'{path}{scope.get("name")}/'

        if scope.get('type') == 'room':
            choices.append((f'{scope["id"]} 0', new_path))

            for room_child in scope.get('scope'):
                if room_child.get('type') == 'folder':
                    choices.append(
                        (f'{scope["id"]} {room_child["id"]}',
                        f'{new_path}{room_child.get("name")}/')
                    )
        else:
            build_client_move_choices(scope, choices, new_path)

def get_client_move_choices(scopes):
    choices = []
    build_client_move_choices(scopes, choices, '')
    return choices

@app.route('/action/client/move', methods=['GET', 'POST'])
@handle_server_errors('scopes')
@login_required
def action_client_move():
    form = ClientMoveForm(request.form)
    if request.method == 'POST':
        ips = form.ips.data.split(' ')
        if not validate_elements(ips):
            return redirect(url_for('scopes'))

        room_id, folder_id = form.scopes.data.split(' ')

        payload = {"clients": ips,
                   "room": int(room_id),
                   "folder_id": int(folder_id)}
        server = get_server_from_clients(ips)
        r = server.post('/client/move', payload)
        flash(_('Client moved successfully'), category='info')
        return redirect(url_for('scopes'))
    else:
        ips = parse_elements(request.args.to_dict())
        form.ips.data = " ".join(ips)

        if not validate_elements(ips):
            return redirect(url_for('scopes'))

        scopes, clients = get_scopes(set(ips))
        form.scopes.choices = get_client_move_choices(scopes)
        selected_clients = list(get_selected_clients(scopes['scope']).items())

        return render_template('actions/client_move.html', form=form,
                                selected_clients=selected_clients,
                                scopes=scopes)


PLACEHOLDER_CLIENT_IMPORT_TEXT = '''client_name1,94:c6:91:a6:25:1a,10.141.10.100
client_name2,94:c6:91:a6:25:1b,10.141.10.101'''

@app.route('/action/clients/import', methods=['GET'])
@handle_server_errors('scopes')
@login_required
def action_clients_import_get():
    params = request.args.to_dict()
    if not params.get('scope-room'):
        flash(_('Please, select one room'), category='error')
        return redirect(url_for('scopes'))

    form = ImportClientsForm()
    form.server.data = params['scope-server']
    server = get_server_from_ip_port(params['scope-server'])
    r = server.get('/scopes')

    rooms = parse_scopes_from_tree(r.json(), 'room')
    selected_room_id = params['scope-room']
    selected_room = [(room['id'], room['name'] + " (" + room['parent'] + ")")
                     for room in rooms if room['id'] == int(selected_room_id)]
    form.room.choices = selected_room
    form.room.render_kw = {'readonly': True}
    repositories = get_repositories(server)
    form.repo.choices = [(repo["id"], repo["name"]) for repo in repositories]
    form.client_conf.render_kw = {'placeholder': PLACEHOLDER_CLIENT_IMPORT_TEXT}

    scopes, _clients = get_scopes()
    return render_template('actions/import_clients.html', form=form,
                           scopes=scopes)


OG_CLIENT_DEFAULT_BOOT = "pxe"
OG_CLIENT_DEFAULT_LIVEDIR = "ogLive"
OG_CLIENT_DEFAULT_MAINTENANCE = False
OG_CLIENT_DEFAULT_NETDRIVER = "generic"
OG_CLIENT_DEFAULT_NETIFACE = "eth0"
OG_CLIENT_DEFAULT_REMOTE = False


@app.route('/action/clients/import', methods=['POST'])
@handle_server_errors('scopes')
@login_required
def action_clients_import_post():
    form = ImportClientsForm(request.form)
    server = get_server_from_ip_port(form.server.data)

    clients = []
    for client_data in form.client_conf.data.splitlines():
        client_values = split_csv(client_data)

        if len(client_values) != 3 or any(not v for v in client_values):
            flash(_('Each client requires 3 values'), category='error')
            return redirect(url_for('scopes'))

        client_name = client_values[0]

        client_mac = client_values[1]
        normalized_mac = normalize_mac(client_mac)

        if not is_valid_normalized_mac(normalized_mac):
            flash(_(f'Invalid MAC address {client_mac}'), category='error')
            return redirect(url_for("scopes"))

        client_ip = client_values[2]

        if not is_valid_ip(client_ip):
            flash(_(f'Invalid IP address {client_ip}'), category='error')
            return redirect(url_for("scopes"))

        clients.append({
            'name': client_name,
            'mac': normalized_mac,
            'ip': client_ip
        })

    if not clients:
        flash(_('No client configuration found'), category='error')
        return redirect(url_for('scopes'))

    payload = {'boot': OG_CLIENT_DEFAULT_BOOT,
               'livedir': OG_CLIENT_DEFAULT_LIVEDIR,
               'maintenance': OG_CLIENT_DEFAULT_MAINTENANCE,
               'netdriver': OG_CLIENT_DEFAULT_NETDRIVER,
               'netiface': OG_CLIENT_DEFAULT_NETIFACE,
               'netmask': "0",
               'remote': OG_CLIENT_DEFAULT_REMOTE,
               "repo_id": int(form.repo.data),
               'room': int(form.room.data)}

    for client in clients:
        for key, value in client.items():
            payload[key] = value
        res = server.post('/client/add', payload)
    flash(_('Clients imported successfully'), category='info')
    return redirect(url_for('scopes'))


def get_selected_clients(scopes):
    selected_clients = dict()

    for scope in scopes:
        scope_type = scope.get('type')
        selected = scope.get('selected')
        if ((scope_type == 'computer') and selected):
            name_id = scope.get('name') + '_' + str(scope.get('id'))
            selected_clients[name_id] = scope.get('ip')[0]
        else:
            selected_clients.update(get_selected_clients(scope['scope']))

    return selected_clients

@app.route('/action/client/delete', methods=['GET', 'POST'])
@handle_server_errors('scopes')
@login_required
def action_client_delete():
    form = GenericForm(request.form)
    if request.method == 'POST':
        ips = form.ips.data.split(' ')
        if not validate_elements(ips):
            return redirect(url_for('scopes'))

        payload = {'clients': ips}
        server = get_server_from_clients(ips)
        r = server.post('/client/delete', payload)
        flash(_('Client deleted successfully'), category='info')
        return redirect(url_for('scopes'))
    else:
        ips = parse_elements(request.args.to_dict())
        form.ips.data = " ".join(ips)
        if validate_elements(ips):
            scopes, clients = get_scopes(set(ips))
            selected_clients = list(get_selected_clients(scopes['scope']).items())
            return render_template('actions/delete_client.html', form=form,
                                   selected_clients=selected_clients,
                                   scopes=scopes)
        else:
            return redirect(url_for('scopes'))

@app.route('/action/client/search', methods=['GET'])
@handle_server_errors('scopes')
@login_required
def action_client_search():
    scopes, clients = get_scopes()
    clients = parse_scopes_from_tree(scopes, 'computer')

    return render_template('actions/client_search.html',
                            scopes=scopes,
                            clients=clients)

@app.route('/action/cmd/run', methods=['GET', 'POST'])
@handle_server_errors('commands')
@login_required
def action_run_cmd():
    form = RunCmdForm(request.form)
    if request.method == 'POST':
        ips = form.ips.data.split(' ')
        if not validate_elements(ips):
            return redirect(url_for('commands'))

        payload = {
            'clients': ips,
            'run': form.command.data,
            'inline': True
        }
        server = get_server_from_clients(ips)
        r = server.post('/shell/run', payload)

        flash(_('Command sent successfully'), category='info')
        return redirect(url_for('commands'))
    else:
        ips = parse_elements(request.args.to_dict())
        form.ips.data = " ".join(ips)
        if not validate_elements(ips):
            return redirect(url_for('commands'))

        scopes, clients = get_scopes(set(ips))
        selected_clients = list(get_selected_clients(scopes['scope']).items())
        return render_template('actions/script_run.html', form=form,
                                selected_clients=selected_clients,
                                scopes=scopes)

@app.route('/action/script/run', methods=['GET', 'POST'])
@handle_server_errors('commands')
@login_required
def action_run_script():
    form = RunScriptForm(request.form)
    if request.method == 'POST':
        ips = form.ips.data.split(' ')
        if not validate_elements(ips):
            return redirect(url_for('commands'))

        arguments = form.arguments.data.split(' ')
        cmd_elems = [form.script.data] + arguments

        payload = {
            'clients': ips,
            'run': ' '.join(cmd_elems),
            'inline': False
        }
        server = get_server_from_clients(ips)
        r = server.post('/shell/run', payload)

        flash(_('Script run sent successfully'), category='info')
        return redirect(url_for('commands'))
    else:
        ips = parse_elements(request.args.to_dict())
        form.ips.data = " ".join(ips)
        if not validate_elements(ips):
            return redirect(url_for('commands'))

        server = get_server_from_clients(ips)
        different_setups = False
        reference_patitioning = None
        for ip in ips:
            r = server.get('/client/setup', payload={'client': [ip]})

            partitions = r.json()['partitions'][1:]
            if not reference_patitioning:
                reference_patitioning = partitions
            elif reference_patitioning != partitions:
                different_setups = True

        if different_setups:
            flash(_('Some clients don\'t have same configuration'), category='info')

        r = server.get('/shell/list')

        scripts = r.json()['scripts']

        for script in scripts:
            form.script.choices.append((script, script))

        scopes, clients = get_scopes(set(ips))
        selected_clients = list(get_selected_clients(scopes['scope']).items())
        return render_template('actions/script_run.html', form=form,
                                selected_clients=selected_clients,
                                scopes=scopes)

@app.route('/action/script/output', methods=['GET'])
@handle_server_errors('commands')
@login_required
def action_script_display_output():
    ips = parse_elements(request.args.to_dict())

    if not validate_elements(ips):
        return redirect(url_for('commands'))

    server = get_server_from_clients(ips)

    r = server.get('/shell/output', payload={'clients': list(ips)})

    client_data = r.json()['clients']

    for client in client_data:
        if not 'output' in client:
            client['output'] = _('No output')

        if not 'retcode' in client:
            client['retcode'] = 0

        if not 'cmd' in client:
            client['cmd'] = ['Null']

        if not 'tstamp' in client:
            client['tstamp'] = _('No time available')
        else:
            timestamp_utc = datetime.datetime.utcfromtimestamp(client['tstamp'])
            client['tstamp'] = timestamp_utc.strftime('%Y-%m-%d  %H:%M:%S')

    client_data.sort(key=lambda x:x['tstamp'], reverse=True)

    scopes, clients = get_scopes(set(ips))
    selected_clients = list(get_selected_clients(scopes['scope']).items())
    return render_template('actions/script_output.html',
                            selected_clients=selected_clients,
                            scopes=scopes, client_data=client_data)

def get_clients_modes(ips, server):
    modes = {}
    for ip in ips:
        r = server.get('/client/info', payload={"client": [ip]})
        resp = r.json()
        current_boot = resp['boot']
        client_name = resp['name']
        if current_boot not in modes:
            modes[current_boot] = [client_name]
        else:
            modes[current_boot].append(client_name)
    return modes

@app.route('/action/mode', methods=['GET', 'POST'])
@handle_server_errors('commands')
@login_required
def action_mode():
    form = BootModeForm(request.form)
    if request.method == 'POST':
        ips = form.ips.data.split(' ')
        payload = { 'clients': ips, 'mode': form.boot.data }

        server = get_server_from_clients(ips)
        r = server.post('/mode', payload)
        flash(_('Client set boot mode request sent successfully'), category='info')
        return redirect(url_for('commands'))

    else:
        ips = parse_elements(request.args.to_dict())
        if not validate_elements(ips):
            return redirect(url_for('commands'))

        form.ips.data = " ".join(ips)
        server = get_server_from_clients(ips)
        modes_set = get_clients_modes(ips, server)
        r = server.get('/mode')

        mode_descriptions = {
            'pxe': 'ogLive (pxe)',
            '11': 'Disk 1 Partition 1 (11)',
            '19pxeADMIN': 'ogLive Debug (19pxeADMIN)',
            '12': 'Disk 1 Partition 2 (12)',
            '13': 'Disk 1 Partition 3 (13)',
            'memtest': 'Memory Test (memtest)',
        }

        excluded_modes = ['00unknown', '10']

        most_used_mode = max(modes_set, key=lambda m: len(modes_set[m]))
        available_modes = []
        if most_used_mode in r.json()['modes']:
            most_used_t = (most_used_mode, mode_descriptions.get(most_used_mode, most_used_mode))
            available_modes.append(most_used_t)

        for mode in r.json()['modes']:
            if mode != most_used_mode and mode not in excluded_modes:
                available_modes.append((mode, mode_descriptions.get(mode, mode)))

        if not available_modes:
            flash(_('no boot templates are available in the server'),
                  category='error')
            return redirect(url_for('commands'))
        form.boot.choices = list(available_modes)

        form.ok.render_kw = { 'formaction': url_for('action_mode') }
        scopes, clients = get_scopes(set(ips))
        selected_clients = list(get_selected_clients(scopes['scope']).items())
        return render_template('actions/mode.html', form=form, scopes=scopes,
                               selected_clients=selected_clients,
                               clients=clients, modes_set=modes_set)

def get_client_list_by_key(ips, server, key):
    ret = {}
    for ip in ips:
        r = server.get('/client/info', payload={"client": [ip]})
        resp = r.json()
        val = resp[key]
        if val not in ret:
            ret[val] = [ip]
        else:
            ret[val].append(ip)
    return ret

@app.route('/action/oglive', methods=['GET', 'POST'])
@handle_server_errors('commands')
@login_required
def action_oglive():
    form = OgliveForm(request.form)
    if request.method == 'POST':
        ips = form.ips.data.split(' ')
        payload = {'clients': ips, 'name': form.oglive.data}
        server = get_server_from_clients(ips)
        r = server.post('/oglive/set', payload)
        flash(_('Client set ogLive request sent successfully'), category='info')
        return redirect(url_for('commands'))

    else:
        ips = parse_elements(request.args.to_dict())
        if not validate_elements(ips):
            return redirect(url_for('commands'))

        form.ips.data = " ".join(ips)
        server = get_server_from_clients(list(ips))

        oglives_set = get_client_list_by_key(ips, server, key='livedir')

        r = server.get('/oglive/list')

        oglive_list = r.json()['oglive']
        most_used_live = max(oglives_set, key=lambda l: len(oglives_set[l]))

        available_oglives = []
        for oglive in oglive_list:
            live_entry = (oglive.get('directory'), oglive.get('directory'))
            if oglive.get('directory') == most_used_live:
                available_oglives.insert(0, live_entry)
            else:
                available_oglives.append(live_entry)

        default_idx = int(r.json()['default'])
        default_oglive_dir = oglive_list[default_idx].get('directory')
        default_live_entry = ('default', f'ogLive ({default_oglive_dir})')

        if most_used_live == OG_CLIENT_DEFAULT_LIVEDIR:
            available_oglives.insert(0, default_live_entry)
        else:
            available_oglives.append(default_live_entry)

        form.oglive.choices = available_oglives

        form.ok.render_kw = {'formaction': url_for('action_oglive')}
        scopes, clients = get_scopes(set(ips))
        selected_clients = list(get_selected_clients(scopes['scope']).items())
        return render_template('actions/oglive.html', oglives_set=oglives_set, form=form, scopes=scopes,
                               selected_clients=selected_clients)


def get_clients_repo_dictionary(ips, server, repositories):
    repos = {}
    repo_id_to_name = {repo["id"]: repo["name"] for repo in repositories}

    for ip in ips:
        r = server.get('/client/info', payload={"client": [ip]})
        resp = r.json()
        repo_name = repo_id_to_name[resp['repo_id']]
        if repo_name not in repos:
            repos[repo_name] = [ip]
        else:
            repos[repo_name].append(ip)
    return repos

@app.route('/action/repo/set', methods=['GET', 'POST'])
@handle_server_errors('commands')
@login_required
def action_repo_set():
    form = SetRepoForm(request.form)
    if request.method == 'POST':
        ips = form.ips.data.split(' ')
        payload = {'clients': ips, 'id': int(form.repo.data)}
        server = get_server_from_clients(ips)
        r = server.post('/client/repo', payload)
        flash(_('Repo set ogLive request sent successfully'), category='info')
        return redirect(url_for('commands'))

    else:
        ips = parse_elements(request.args.to_dict())
        if not validate_elements(ips):
            return redirect(url_for('commands'))

        form.ips.data = " ".join(ips)
        server = get_server_from_clients(list(ips))
        repositories = get_repositories(server)
        repos_set = get_clients_repo_dictionary(ips, server, repositories)

        scopes, clients = get_scopes(set(ips))

        repo_set = get_client_list_by_key(ips, server, key='repo_id')

        most_used_repo_id = max(repo_set, key=lambda id: len(repo_set[id]))

        form.repo.choices = []
        for repo in repositories:
            repo_entry = (repo["id"], repo["name"])
            if repo["id"] == most_used_repo_id:
                form.repo.choices.insert(0, repo_entry)
            else:
                form.repo.choices.append(repo_entry)

        selected_clients = list(get_selected_clients(scopes['scope']).items())
        return render_template('actions/repo_set.html', repos_set=repos_set, form=form, scopes=scopes,
                               selected_clients=selected_clients)


@app.route('/action/image/create', methods=['GET', 'POST'])
@handle_server_errors('commands')
@login_required
def action_image_create():
    form = ImageCreateForm(request.form)
    if request.method == 'POST':
        ip = form.ip.data
        server = get_server_from_clients([ip])
        r = server.get('/client/info', payload={"client": [ip]})

        image_name = remove_accents(form.name.data.strip())
        if ' ' in image_name:
            flash(_('No spaces allowed in image names'), category='error')
            return redirect(url_for('commands'))

        disk, partition, code = form.os.data.split(' ')
        payload = {"clients": [ip],
                   "disk": disk,
                   "partition": partition,
                   "code": code,
                   "name": image_name,
                   "repository_id": int(form.repository.data),
                   "id": "0", # This is ignored by the server.
                   "description": form.description.data,
                   "group_id": 0, # Default group.
                   "center_id": r.json()["center"]}
        r = server.post('/image/create', payload)
        return redirect(url_for('commands'))
    else:
        ips = parse_elements(request.args.to_dict())
        form.ip.data = " ".join(ips)
        if not validate_elements(ips, max_len=1):
            return redirect(url_for('commands'))

        server = get_server_from_clients(ips)
        r = server.get('/client/setup', payload={'client': list(ips)})

        invalid_part_types = get_invalid_image_partition_types()

        for part in r.json()['partitions'][1:]:
            part_type = PART_TYPE_CODES.get(int(part.get('code')), 'UNKNOWN')

            if part_type in invalid_part_types:
                continue

            if part.get('disk') != 1:
                continue

            form.os.choices.append(
                (f"{part.get('disk')} {part.get('partition')} {part.get('code')}",
                 f"Disk {part.get('disk')} | Partition {part.get('partition')} "
                 f"| {PART_TYPE_CODES.get(part.get('code'), 'UNKNOWN')} "
                 f"{FS_CODES.get(part.get('filesystem'), 'UNKNOWN')}")
            )
        r = server.get('/client/info', payload={'client': list(ips)})

        client_repo_id = r.json()['repo_id']
        repositories = get_repositories(server)
        form.repository.choices = [ (repo['id'], repo['name']) for repo in repositories
                                   if client_repo_id == repo['id']]
        form.repository.render_kw = {'readonly': True}

        scopes, clients = get_scopes(set(ips))
        selected_clients = list(get_selected_clients(scopes['scope']).items())

        return render_template('actions/image_create.html', form=form,
                               scopes=scopes,
                               selected_clients=selected_clients)


@app.route('/action/image/update', methods=['GET', 'POST'])
@handle_server_errors('commands')
@login_required
def action_image_update():
    form = ImageUpdateForm(request.form)
    if request.method == 'POST':
        ip = form.ip.data
        disk, partition, code = form.os.data.split(' ')
        image_id = form.image.data
        server = get_server_from_clients([ip])
        r = server.get('/images')

        images_list = r.json()['images']
        image = search_image(images_list, int(image_id))
        if not image:
            flash(_('Image to restore was not found'), category='error')
            return redirect(url_for('commands'))

        repository = get_repository(image['repo_id'], server)
        payload = {'clients': [ip],
                   'disk': disk,
                   'partition': partition,
                   'code': code,
                   'name': image['name'],
                   'repository_id': int(repository['id']),
                   'id': str(image['id']),
                   'backup': form.backup.data,
                   # Dummy parameters, not used by ogServer on image update.
                   'group_id': 0,
                   'center_id': 0}
        r = server.post('/image/update', payload)
        flash(_('Image update command sent sucessfully'), category='info')
        return redirect(url_for('commands'))

    params = request.args.to_dict()
    ips = parse_elements(params)
    if not validate_elements(ips, max_len=1):
        return redirect(url_for('commands'))

    center_id = int(params.get('scope-center'))
    form.ip.data = ' '.join(ips)

    server = get_server_from_clients(ips)
    r = server.get('/client/info', payload={'client': list(ips)})

    repo_id = r.json()['repo_id']
    images = get_images_from_repo(server, repo_id)

    if not images:
        flash(_(f'Computer(s) assigned to a repo with no images'), category='error')
        return redirect(url_for('commands'))

    images = filter_images_allowed_in_center(server, images, center_id)

    sort_images(images)

    for image in images:
        form.image.choices.append((image['id'], image['name']))

    r = server.get('/client/setup', payload={'client': list(ips)})

    invalid_part_types = get_invalid_image_partition_types()

    part_content = {}
    for part in r.json()['partitions'][1:]:
        part_type = PART_TYPE_CODES.get(int(part.get('code')), 'UNKNOWN')

        if part_type in invalid_part_types:
            continue

        if part.get('disk') != 1:
                continue

        partition_value = f"{part.get('disk')} {part.get('partition')} {part.get('code')}"
        partition_text = f"Disk {part.get('disk')} | Partition {part.get('partition')} "
        f"| {PART_TYPE_CODES.get(part.get('code'), 'UNKNOWN')} "
        f"{FS_CODES.get(part.get('filesystem'), 'UNKNOWN')}"

        form.os.choices.append(
            (partition_value,
             f"Disk {part.get('disk')} | Partition {part.get('partition')} "
             f"| {PART_TYPE_CODES.get(part.get('code'), 'UNKNOWN')} "
             f"{FS_CODES.get(part.get('filesystem'), 'UNKNOWN')}")
        )

        if part['image']:
            for image in images:
                if image['id'] == part['image']:
                    part_content[partition_value] = part['image']
                    break

    scopes, _clients = get_scopes(set(ips))
    selected_clients = list(get_selected_clients(scopes['scope']).items())

    return render_template('actions/image_update.html', form=form,
                           selected_clients=selected_clients,
                           scopes=scopes, part_content=part_content)


@app.route('/action/reboot', methods=['GET', 'POST'])
@handle_server_errors('commands')
@login_required
def action_reboot():
    form = GenericForm(request.form)
    if request.method == 'POST':
        ips = form.ips.data.split(' ')
        if not validate_elements(ips):
            return redirect(url_for('commands'))

        payload = {'clients': ips}
        server = get_server_from_clients(ips)
        r = server.post('/reboot', payload)
        flash(_('Client rebooted successfully'), category='info')
        return redirect(url_for('commands'))
    else:
        ips = parse_elements(request.args.to_dict())
        form.ips.data = " ".join(ips)
        if validate_elements(ips):
            scopes, clients = get_scopes(set(ips))
            selected_clients = list(get_selected_clients(scopes['scope']).items())
            return render_template('actions/reboot.html', form=form,
                                   selected_clients=selected_clients,
                                   scopes=scopes)
        else:
            return redirect(url_for('commands'))


@app.route('/action/refresh', methods=['POST'])
@handle_server_errors('commands')
@login_required
def action_refresh():
    ips = parse_elements(request.form.to_dict())
    if not validate_elements(ips):
        return redirect(url_for('commands'))

    server = get_server_from_clients(list(ips))
    payload = {'clients': list(ips)}
    r = server.post('/refresh', payload)
    flash(_('Refresh request processed successfully'), category='info')
    return redirect(url_for('commands'))

@app.route('/action/center/add', methods=['GET', 'POST'])
@handle_server_errors('scopes')
@login_required
def action_center_add():
    form = CenterForm(request.form)
    if request.method == 'POST':
        payload = {"name": form.name.data,
                   "comment": form.comment.data}
        server = get_server_from_ip_port(form.server.data)
        r = server.post('/center/add', payload)
        flash(_('Center added successfully'), category='info')
        return redirect(url_for("scopes"))
    else:
        server_choices = [(server.ip + ':' + str(server.port), server.name)
                          for server in servers]
        form.server.choices = server_choices
        scopes, clients = get_scopes()
        return render_template('actions/add_center.html', form=form,
                               scopes=scopes)

@app.route('/action/center/update', methods=['GET', 'POST'])
@handle_server_errors('scopes')
@login_required
def action_center_update():
    form = CenterForm(request.form)
    if request.method == 'POST':
        payload = {"id": int(form.center.data),
                   "name": form.name.data,
                   "comment": form.comment.data}
        server = get_server_from_ip_port(form.server.data)
        r = server.post('/center/update', payload)
        flash(_('Center updated successfully'), category='info')
        return redirect(url_for("scopes"))
    else:
        params = request.args.to_dict()
        server = params.get('scope-server')
        center = params.get('scope-center')
        if not center:
            flash(_('Please, select one center'), category='error')
            return redirect(url_for("scopes"))
        if not server:
            flash(_('Internal error: server was not sent as request argument'),
                  category='error')
            return redirect(url_for("scopes"))

        server = get_server_from_ip_port(server)

        form.server.choices = [(server.ip + ':' + str(server.port), server.name)]
        form.server.render_kw = {'readonly': True}
        form.center.data = center

        payload = {"id": int(center)}
        r = server.get('/center/info', payload)

        form.comment.data = r.json()['comment']
        form.name.data = r.json()['name']

        scopes, clients = get_scopes()
        form.submit.render_kw = {"formaction": url_for('action_center_update')}
        return render_template('actions/center_update.html', form=form,
                               scopes=scopes)

@app.route('/action/center/info', methods=['GET'])
@handle_server_errors('scopes')
@login_required
def action_center_info():
    form = CenterForm(request.form)
    params = request.args.to_dict()
    server = params.get('scope-server')
    center = params.get('scope-center')
    if not center:
        flash(_('Please, select one center'), category='error')
        return redirect(url_for("scopes"))
    if not server:
        flash(_('Internal error: server was not sent as request argument'),
                category='error')
        return redirect(url_for("scopes"))

    server = get_server_from_ip_port(server)

    form.server.choices = [(server.ip + ':' + str(server.port), server.name)]
    form.server.render_kw = {'readonly': True}
    form.center.data = center
    form.center.render_kw = {'readonly': True}

    payload = {"id": int(center)}
    r = server.get('/center/info', payload)

    form.comment.data = r.json()['comment']
    form.comment.render_kw = {'readonly': True}
    form.name.data = r.json()['name']
    form.name.render_kw = {'readonly': True}
    form.submit.render_kw = {'readonly': True, 'hidden': True}

    scopes, clients = get_scopes()
    return render_template('actions/center_details.html', form=form,
                            scopes=scopes)

def get_scope_context_rec(elem_id, elem_type, scopes, ancestors):
    if not scopes:
        return ([], None)
    res = None
    for s in scopes:
        if s['type'] == elem_type and int(s['id']) == elem_id:
            ancestors.append(s['name'])
            return (ancestors, s)
        ancestors_tmp = list(ancestors)
        ancestors_tmp.append(s['name'])
        ancestors_tmp, elem = get_scope_context_rec(elem_id, elem_type, s['scope'], ancestors_tmp)
        if elem:
            res = (ancestors_tmp, elem)
            break
    if res:
        return res
    else:
        return ([], None)

def find_element_scope(elem_id, elem_type, scopes):
    unused, elem = get_scope_context_rec(elem_id, elem_type, scopes['scope'], [])
    return elem

def get_scope_context(elem_id, elem_type, scopes):
    ancestors, elem = get_scope_context_rec(elem_id, elem_type, scopes['scope'], [])
    children = {}
    for c in elem['scope']:
        if c['type'] not in children:
            children[c['type']] = []
        children[c['type']].append(c['name'])
    return (ancestors, children)

@app.route('/action/center/delete', methods=['GET', 'POST'])
@handle_server_errors('scopes')
@login_required
def action_center_delete():
    form = DeleteCenterForm(request.form)
    if request.method == 'POST':
        server = get_server_from_ip_port(form.server.data)
        payload = {"id": form.center.data}
        r = server.post('/center/delete', payload)
        flash(_('Center deleted successfully'), category='info')
        return redirect(url_for("scopes"))
    else:
        params = request.args.to_dict()
        if not params.get('scope-center'):
            flash(_('Please, select one center'), category='error')
            return redirect(url_for('scopes'))
        server = get_server_from_ip_port(params['scope-server'])
        r = server.get('/scopes')

        form.center.data = params['scope-center']
        form.server.data = params['scope-server']
        scopes, clients = get_scopes()
        ancestors, children = get_scope_context(int(params['scope-center']), 'center', scopes)
        return render_template('actions/delete_center.html', form=form,
                               scopes=scopes, ancestors=ancestors, children=children)

@app.route('/action/room/add', methods=['GET', 'POST'])
@handle_server_errors('scopes')
@login_required
def action_room_add():
    form = RoomForm(request.form)
    if request.method == 'POST':
        server = get_server_from_ip_port(form.server.data)
        payload = {"center": int(form.center.data),
                   "name": form.name.data,
                   "netmask": form.netmask.data,
                   "gateway": form.gateway.data,
                   "folder_id": int(form.folder_id.data)}
        r = server.post('/room/add', payload)
        flash(_('Room added successfully'), category='info')
        return redirect(url_for("scopes"))
    else:
        params = request.args.to_dict()
        if not params.get('scope-center'):
            flash(_('Please, select a center or a folder'), category='error')
            return redirect(url_for('scopes'))
        server = get_server_from_ip_port(params['scope-server'])
        r = server.get('/scopes')

        selected_center_id = params['scope-center']
        centers = parse_scopes_from_tree(r.json(), 'center')
        selected_center = [(center['id'], center['name']) for center in centers
                           if center['id'] == int(selected_center_id)]
        form.center.choices = selected_center
        form.center.render_kw = {'readonly': True}
        form.server.data = params['scope-server']

        if params.get('folder'):
            form.folder_id.data = params['folder']
        else:
            form.folder_id.data = 0

        scopes, clients = get_scopes()
        return render_template('actions/add_room.html', form=form,
                               scopes=scopes)

@app.route('/action/room/update', methods=['GET', 'POST'])
@handle_server_errors('scopes')
@login_required
def action_room_update():
    form = RoomForm(request.form)
    if request.method == 'POST':
        server = get_server_from_ip_port(form.server.data)
        payload = {"name": form.name.data,
                   "netmask": form.netmask.data,
                   "gateway": form.gateway.data,
                   "id": int(form.room.data)}
        r = server.post('/room/update', payload)
        flash(_('Room updated successfully'), category='info')
        return redirect(url_for("scopes"))
    else:
        params = request.args.to_dict()
        room_id = params.get('scope-room')
        if not room_id:
            flash(_('Please, select a room to update'), category='error')
            return redirect(url_for('scopes'))
        server = get_server_from_ip_port(params['scope-server'])

        del form.center
        form.server.data = params['scope-server']
        form.room.data = room_id

        payload = {"id": int(room_id)}
        r = server.get('/room/info', payload)
        form.name.data = r.json()['name']
        form.gateway.data = r.json()['gateway']
        form.netmask.data = r.json()['netmask']

        form.submit.render_kw = {"formaction": url_for('action_room_update')}

        scopes, clients = get_scopes()
        return render_template('actions/room_update.html', form=form,
                               scopes=scopes)

@app.route('/action/room/info', methods=['GET'])
@handle_server_errors('scopes')
@login_required
def action_room_info():
    form = RoomForm(request.form)
    params = request.args.to_dict()
    room_id = params.get('scope-room')
    if not room_id:
        flash(_('Please, select a room to update'), category='error')
        return redirect(url_for('scopes'))
    server = get_server_from_ip_port(params['scope-server'])

    del form.center

    payload = {"id": int(room_id)}
    r = server.get('/room/info', payload)
    form.name.data = r.json()['name']
    form.name.render_kw = {'readonly': True}
    form.gateway.data = r.json()['gateway']
    form.gateway.render_kw = {'readonly': True}
    form.netmask.data = r.json()['netmask']
    form.netmask.render_kw = {'readonly': True}

    form.submit.render_kw = {'readonly': True, 'hidden': True}

    scopes, clients = get_scopes()
    return render_template('actions/room_details.html', form=form,
                            scopes=scopes)

@app.route('/action/room/delete', methods=['GET', 'POST'])
@handle_server_errors('scopes')
@login_required
def action_room_delete():
    form = DeleteRoomForm(request.form)
    if request.method == 'POST':
        payload = {"id": form.room.data}
        server = get_server_from_ip_port(form.server.data)
        r = server.post('/room/delete', payload)
        flash(_('Room deleted successfully'), category='info')
        return redirect(url_for("scopes"))
    else:
        params = request.args.to_dict()
        if not params.get('scope-room'):
            flash(_('Please, select one room'), category='error')
            return redirect(url_for('scopes'))
        server = get_server_from_ip_port(params['scope-server'])
        r = server.get('/scopes')

        form.room.data = params['scope-room']
        form.room.render_kw = {'readonly': True}
        form.server.data = params['scope-server']
        scopes, clients = get_scopes()
        ancestors, children = get_scope_context(int(params['scope-room']), 'room', scopes)
        return render_template('actions/delete_room.html', form=form,
                               scopes=scopes, ancestors=ancestors, children=children)

@app.route('/commands/', methods=['GET'])
@login_required
def commands():
    try:
        scopes, clients = get_scopes()
    except ServerError:
        scopes = {}
        clients = {}
    return render_template('commands.html', scopes=scopes, clients=clients)

def _get_sorted_repos(repos):
    return dict(sorted(repos.items(), key=lambda item: item[1]['name']))

def get_images_grouped_by_repos():
    responses = multi_request('get', '/images')
    servers=[]
    for resp in responses:
        server={}
        server['server'] = resp['server']
        images=resp['json']['images']
        all_repos=get_repositories(resp['server'])
        repos={}

        sort_images(images)

        for image in images:
            repo_id=image['repo_id']
            repo_data={}
            if repo_id not in repos:
                image_repo = [repo['name'] for repo in all_repos
                             if repo_id == repo['id']]
                repos[repo_id] = {}
                if image_repo:
                    repos[repo_id]['name'] = image_repo[0]
                else:
                    repos[repo_id]['name'] = 'unknown'
                repos[repo_id]['images'] = [image]
            else:
                repos[repo_id]['images'].append(image)
        server['repos'] = _get_sorted_repos(repos)
        servers.append(server)
    return servers

@app.route('/images/', methods=['GET'])
@login_required
def images():
    try:
        responses = get_images_grouped_by_repos()
    except ServerError:
        responses = []
    return render_template('images.html', responses=responses)


@app.route('/repos/', methods=['GET'])
@login_required
def manage_repos():
    try:
        responses = get_all_repositories()
    except ServerError:
        responses = []
    return render_template('repos.html', repos_resp=responses)

def repo_addr_is_valid(form):
    invalid_ips = []
    empty_ips = 0
    ip_count = 0
    for ip in form.addr:
        ip_count += 1
        ip = ip.data.strip()
        if not ip:
            empty_ips += 1
            continue
        if not is_valid_ip(ip):
            invalid_ips.append('"' + ip + '"')

    res = True
    if empty_ips > 0:
        res = False
        flash(_(f'{empty_ips} addresses are invalid'), category='error')

    if invalid_ips:
        res = False
        flash(_(f'The following addresses are invalid: {" ".join(invalid_ips)}'), category='error')

    MAX_IP_COUNT = 128
    if ip_count > MAX_IP_COUNT:
        res = False
        flash(_(f'More than {MAX_IP_COUNT} addresses is not supported'), category='error')

    return res

@app.route('/action/repo/add', methods=['POST', 'GET'])
@handle_server_errors('manage_repos')
@login_required
def action_repo_add():
    form = RepoForm(request.form)
    if request.method == 'POST':
        if not form.validate():
            flash(form.errors, category='error')
            return redirect(url_for('manage_repos'))

        if not repo_addr_is_valid(form):
            return redirect(url_for('manage_repos'))

        addr = [ip.data.strip() for ip in form.addr]

        payload = {"name": form.name.data,
                   "addr": addr,
                   "center": 1}
        server = get_server_from_ip_port(form.server.data)
        r = server.post('/repository/add', payload)
        flash(_('Repo added successfully'), category='info')
        return redirect(url_for("manage_repos"))
    else:
        params = request.args.to_dict()
        if not params.get('repos-server'):
            flash(_('Please, select a server'), category='error')
            return redirect(url_for('manage_repos'))

        form.server.data = params['repos-server']
        form.addr.append_entry('')

        responses = get_all_repositories()
        return render_template('actions/repos_add.html', form=form,
                               repos_resp=responses)

@app.route('/action/repo/update', methods=['GET', 'POST'])
@handle_server_errors('manage_repos')
@login_required
def action_repo_update():
    form = RepoForm(request.form)
    if request.method == 'POST':
        server = get_server_from_ip_port(form.server.data)

        if not repo_addr_is_valid(form):
            return redirect(url_for('manage_repos'))

        addr = [ip.data.strip() for ip in form.addr]

        payload = { 'id': int(form.repo_id.data),
                   'name': form.name.data,
                   'addr': addr,
                   'center': 1}
        r = server.post('/repository/update', payload)
        flash(_('Repo updated successfully'), category='info')
        return redirect(url_for('manage_repos'))
    else:
        params = request.args.to_dict()
        repos = parse_elements(params)
        if not validate_elements(repos, max_len=1):
            return redirect(url_for('manage_repos'))
        repo_id = repos.pop()
        server_ip_port = params.get('repos-server')

        if not server_ip_port:
            flash(_('Please, select a server'), category='error')
            return redirect(url_for('manage_repos'))
        server = get_server_from_ip_port(server_ip_port)
        repository = get_repository(int(repo_id), server)

        form.server.data = server_ip_port
        form.repo_id.data = repo_id
        form.name.data = repository['name']
        for addr in repository['addr']:
            form.addr.append_entry(addr)

        responses = get_all_repositories()
        return render_template('actions/repos_update.html', form=form,
                               repos_resp=responses)

@app.route('/action/repo/delete', methods=['GET', 'POST'])
@handle_server_errors('manage_repos')
@login_required
def action_repo_delete():
    form = RepoForm(request.form)
    if request.method == 'POST':
        server = get_server_from_ip_port(form.server.data)
        payload = { 'id': int(form.repo_id.data) }
        r = server.post('/repository/delete', payload)

        flash(_('Repo deleted successfully'), category='info')
        return redirect(url_for('manage_repos'))
    else:
        params = request.args.to_dict()
        repos = parse_elements(params)

        if not validate_elements(repos, max_len=1):
            return redirect(url_for('manage_repos'))
        repo_id = repos.pop()
        if not repo_id:
            flash(_('Please, select a repo'), category='error')
            return redirect(url_for('manage_repos'))
        repo_id = int(repo_id)
        server_ip_port = params.get('repos-server')

        if not server_ip_port:
            flash(_('Please, select a server'), category='error')
            return redirect(url_for('manage_repos'))
        server = get_server_from_ip_port(server_ip_port)
        repository = get_repository(repo_id, server)
        form.server.data = server_ip_port
        form.repo_id.data = repo_id
        form.name.data = repository['name']
        form.name.render_kw = {'readonly': True}
        form.name.data = repository['name']
        for addr in repository['addr']:
            form.addr.append_entry(addr)
        for field in form.addr:
            field.render_kw = {'readonly': True}
        responses = get_all_repositories()
        return render_template('actions/delete_repo.html', form=form,
                               repos_resp=responses)


@app.route('/action/repo/info', methods=['GET'])
@handle_server_errors('manage_repos')
@login_required
def action_repo_info():
        form = RepoForm()
        params = request.args.to_dict()
        repos = parse_elements(params)
        if not validate_elements(repos, max_len=1):
            return redirect(url_for('manage_repos'))
        repo_id = repos.pop()
        if not repo_id:
            flash(_('Please, select a repo'), category='error')
            return redirect(url_for('manage_repos'))
        repo_id = int(repo_id)
        server_ip_port = params.get('repos-server')

        if not server_ip_port:
            flash(_('Please, select a server'), category='error')
            return redirect(url_for('manage_repos'))
        server = get_server_from_ip_port(server_ip_port)
        repository = get_repository(repo_id, server)
        form.name.data = repository['name']
        form.name.render_kw = {'readonly': True}
        for addr in repository['addr']:
            form.addr.append_entry(addr)
        for field in form.addr:
            field.render_kw = {'readonly': True}
        form.submit.render_kw = {"style": "visibility:hidden;"}
        responses = get_all_repositories()
        return render_template('actions/repo_details.html', form=form,
                               repos_resp=responses)

@app.route('/servers/', methods=['GET'])
@login_required
def manage_servers():
    return render_template('servers.html', servers=servers)

@app.route('/server/update', methods=['GET'])
@handle_server_errors('manage_servers')
@login_required
def server_update_get():
    params = request.args.to_dict()
    try:
        selected_server = get_server_from_ip_port(params['selected-server'])
    except KeyError:
        flash(_('Please, select one server'), category='error')
        return redirect(url_for('manage_servers'))

    r = selected_server.get('/server')

    form = ServerConfigurationForm()
    server_config = r.json()['servers']
    for c in server_config:
        form.addr.append_entry(c['address'])

    form.server_addr.data = selected_server.ip + ":" + str(selected_server.port)

    return render_template('actions/server_update.html', form=form,
                           servers=servers)

@app.route('/server/update', methods=['POST'])
@handle_server_errors('manage_servers')
@login_required
def server_update_post():
    form = ServerConfigurationForm(request.form)
    try:
        server = get_server_from_ip_port(form.server_addr.data)
    except Exception:
        flash(_('Server {} does not exist').format(form.server_addr.data),
              category='error')
        return redirect(url_for('manage_servers'))

    addr_list = [ip.data.strip() for ip in form.addr]
    invalid_ips = []
    for ip in addr_list:
        if not is_valid_ip(ip):
            invalid_ips.append('"' + ip + '"')

    if invalid_ips:
        flash(_(f'The following addresses are invalid: {" ".join(invalid_ips)}'), category='error')
        return redirect(url_for('manage_servers'))

    r = server.get('/server')
    server_config = r.json()['servers']

    # Remove
    for c in server_config:
        if c['address'] in addr_list:
            continue

        payload = {'id': c['id']}
        rd = server.delete('/server', payload=payload)

    # Add
    for ip in addr_list:
        found = False
        for c in server_config:
            if ip == c['address']:
                found = True
                break

        if found:
            continue

        payload = {'address': ip}
        ra = server.post('/server', payload=payload)

    flash(_('Server update request sent successfully'), category='info')
    return redirect(url_for('manage_servers'))


@app.route('/server/add', methods=['GET'])
@handle_server_errors('manage_servers')
@login_required
def server_add_get():
    form = ServerForm()
    return render_template('actions/add_server.html', form=form,
                           servers=servers)


@app.route('/server/add', methods=['POST'])
@handle_server_errors('manage_servers')
@login_required
def server_add_post():
    form = ServerForm(request.form)
    if not form.validate():
        flash(form.errors, category='error')
        return redirect(url_for('manage_servers'))
    ip_port_str = form.ip.data + ":" + form.port.data
    try:
        get_server_from_ip_port(ip_port_str)
        flash(_('Server {} already exists').format(ip_port_str),
              category='error')
        return redirect(url_for('manage_servers'))
    except Exception:
        return save_server(form)


@app.route('/server/delete', methods=['GET'])
@handle_server_errors('manage_servers')
@login_required
def server_delete_get():
    params = request.args.to_dict()
    try:
        selected_server = get_server_from_ip_port(params['selected-server'])
    except KeyError:
        flash(_('Please, select one server'), category='error')
        return redirect(url_for('manage_servers'))

    form = ServerForm()
    form.name.data = selected_server.name
    form.name.render_kw = {'readonly': True}
    form.ip.data = selected_server.ip
    form.ip.render_kw = {'readonly': True}
    form.port.data = selected_server.port
    form.port.render_kw = {'readonly': True}
    form.api_token.data = selected_server.api_token
    form.api_token.render_kw = {'readonly': True}
    return render_template('actions/delete_server.html', form=form,
                           servers=servers)


@app.route('/server/delete', methods=['POST'])
@handle_server_errors('manage_servers')
@login_required
def server_delete_post():
    form = ServerForm(request.form)
    if not form.validate():
        flash(form.errors, category='error')
        return redirect(url_for('manage_servers'))

    ip_port_str = form.ip.data + ":" + form.port.data
    try:
        server = get_server_from_ip_port(ip_port_str)
        return delete_server(server)
    except Exception:
        flash(_('Server {} does not exist').format(ip_port_str),
              category='error')
        return redirect(url_for('manage_servers'))

@app.route('/lives/', methods=['GET'])
@login_required
def manage_lives():
    try:
        oglive_list = multi_request('get', '/oglive/list')
    except ServerError:
        oglive_list = []
    return render_template('lives.html', oglive_list=oglive_list, servers=servers)

@app.route('/action/live/default', methods=['GET', 'POST'])
@handle_server_errors('manage_lives')
@login_required
def action_live_default():
    form = OgliveForm(request.form)
    if request.method == 'POST':
        try:
            server = get_server_from_ip_port(form.server.data)
        except KeyError:
            flash(_('Please, select one server'), category='error')
            return redirect(url_for('manage_lives'))
        payload = {'name': form.oglive.data}
        r = server.post('/oglive/default', payload)
        flash(_('Set default ogLive request sent successfully'), category='info')
        return redirect(url_for('manage_lives'))
    else:
        params = request.args.to_dict()
        try:
            server = get_server_from_ip_port(params['server'])
        except KeyError:
            flash(_('Please, select one server'), category='error')
            return redirect(url_for('manage_lives'))
        r = server.get('/oglive/list')

        oglive_list = r.json()['oglive']

        available_oglives = []
        for oglive in oglive_list:
            live_entry = (oglive.get('directory'), oglive.get('directory'))
            available_oglives.append(live_entry)

        default_idx = int(r.json()['default'])
        default_live = oglive_list[default_idx].get('directory')

        form.oglive.choices = available_oglives
        form.server.data = params['server']
        form.ok.render_kw = {'formaction': url_for('action_live_default')}

        oglive_list = multi_request('get', '/oglive/list')
        return render_template('actions/live_default.html', oglive_list=oglive_list, form=form, default_live=default_live)

@app.route('/users/', methods=['GET'])
@login_required
def users():
    users = app.config['USERS']
    return render_template('users.html', users=users)


def get_available_centers():
    responses = multi_request('get', '/scopes')
    available_centers = list()
    for resp in responses:
        centers = parse_scopes_from_tree(resp['json'], 'center')
        centers = [(center['id'], center['name']) for center in centers]
        available_centers.extend(centers)
    return available_centers

def get_center_choices():
    responses = multi_request('get', '/scopes')
    available_scopes = list()
    for resp in responses:
        centers = parse_scopes_from_tree(resp['json'], 'center')
        centers = [(str(center['id']), center['name']) for center in centers]
        available_scopes.extend(centers)
    return available_scopes


def save_server(form):
    server_dict = {
        'NAME': form.name.data,
        'IP': form.ip.data,
        'PORT': int(form.port.data),
        'API_TOKEN': form.api_token.data,
    }
    server_obj = OGServer(form.name.data,
                          form.ip.data,
                          int(form.port.data),
                          form.api_token.data)

    filename = os.path.join(app.root_path, ogcp_cfg_path)
    with open(filename, 'r+') as file:
        config = json.load(file)

        try:
            config['SERVERS'].append(server_dict)
        except KeyError:
            config['SERVERS'] = list()
            config['SERVERS'].append(server_dict)

        file.seek(0)
        json.dump(config, file, indent='\t')
        file.truncate()

    servers.append(server_obj)

    return redirect(url_for('manage_servers'))


def delete_server(server):
    server_dict = {
        'NAME': server.name,
        'IP': server.ip,
        'PORT': int(server.port),
        'API_TOKEN': server.api_token,
    }

    filename = os.path.join(app.root_path, ogcp_cfg_path)
    with open(filename, 'r+') as file:
        config = json.load(file)

        try:
            config['SERVERS'].remove(server_dict)
        except (KeyError, ValueError):
            config.pop('IP')
            config.pop('PORT')
            config.pop('API_TOKEN')

        file.seek(0)
        json.dump(config, file, indent='\t')
        file.truncate()

    servers.remove(server)

    return redirect(url_for('manage_servers'))


def save_user(form, preserve_pwd):
    username = form.username.data

    if preserve_pwd:
        pwd_hash = form.pwd.data
    else:
        pwd_hash = hash_password(form.pwd.data)
        pwd_hash_confirm = hash_password(form.pwd_confirm.data)
        if not pwd_hash == pwd_hash_confirm:
            flash(_('Passwords do not match'), category='error')
            return False

    admin = form.admin.data
    scopes = form.scopes.data

    user = {
        'USER': username,
        'PASS': pwd_hash,
        'ADMIN': admin,
        'SCOPES': scopes,
        'PERMISSIONS': {
            'CLIENT': {
                'ADD': form.client_permissions.add.data,
                'UPDATE': form.client_permissions.update.data,
                'DELETE': form.client_permissions.delete.data,
            },
            'CENTER': {
                'ADD': form.center_permissions.add.data,
                'UPDATE': form.center_permissions.update.data,
                'DELETE': form.center_permissions.delete.data,
            },
            'ROOM': {
                'ADD': form.room_permissions.add.data,
                'UPDATE': form.room_permissions.update.data,
                'DELETE': form.room_permissions.delete.data,
            },
            'FOLDER': {
                'ADD': form.folder_permissions.add.data,
                'UPDATE': form.folder_permissions.update.data,
                'DELETE': form.folder_permissions.delete.data,
            },
            'IMAGE': {
                'ADD': form.image_permissions.add.data,
                'UPDATE': form.image_permissions.update.data,
                'DELETE': form.image_permissions.delete.data,
            },
            'REPOSITORY': {
                'ADD': form.repository_permissions.add.data,
                'UPDATE': form.repository_permissions.update.data,
                'DELETE': form.repository_permissions.delete.data,
            },
        },
    }

    filename = os.path.join(app.root_path, ogcp_cfg_path)
    with open(filename, 'r+') as file:
        config = json.load(file)

        old_user = get_user(username)

        if old_user:
            config['USERS'].remove(old_user)
        config['USERS'].append(user)

        file.seek(0)
        json.dump(config, file, indent='\t')
        file.truncate()

    if old_user:
        app.config['USERS'].remove(old_user)
    app.config['USERS'].append(user)

    return True


def delete_user(username):
    user = get_user(username)

    filename = os.path.join(app.root_path, ogcp_cfg_path)
    with open(filename, 'r+') as file:
        config = json.load(file)

        config['USERS'].remove(user)

        file.seek(0)
        json.dump(config, file, indent='\t')
        file.truncate()

    app.config['USERS'].remove(user)

    return redirect(url_for('users'))


@app.route('/user/add', methods=['GET'])
@handle_server_errors('users')
@login_required
def user_add_get():
    form = UserForm()
    form.scopes.choices = get_available_centers()
    return render_template('auth/add_user.html', form=form)


@app.route('/user/add', methods=['POST'])
@handle_server_errors('users')
@login_required
def user_add_post():
    form = UserForm(request.form)
    form.scopes.choices = get_center_choices()
    if not form.validate():
        flash(form.errors, category='error')
        return redirect(url_for('users'))

    if get_user(form.username.data):
        flash(_('This username already exists'), category='error')
        return redirect(url_for('users'))

    if save_user(form, preserve_pwd=False):
        flash(_('User created successfully'), category='info')
    return redirect(url_for('users'))


@app.route('/user/edit', methods=['GET'])
@handle_server_errors('users')
@login_required
def user_edit_get():
    username_set = parse_elements(request.args.to_dict())
    if not validate_elements(username_set, max_len=1):
        return redirect(url_for('users'))

    username = username_set.pop()
    user = get_user(username)
    if not user:
        flash(_('User {} does not exist').format(username), category='error')
        return redirect(url_for('users'))

    form = EditUserForm()
    form.username.data = user.get('USER')
    form.username.render_kw = {'readonly': True}
    form.admin.data = user.get('ADMIN')
    form.scopes.data = user.get('SCOPES')

    if 'PERMISSIONS' in user:
        permissions = user.get('PERMISSIONS')

        def get_permission(target, action):
            if not target in permissions:
                return True
            return permissions[target].get(action, True)

        form.client_permissions.add.data    = get_permission('CLIENT', 'ADD')
        form.client_permissions.update.data = get_permission('CLIENT', 'UPDATE')
        form.client_permissions.delete.data = get_permission('CLIENT', 'DELETE')
        form.center_permissions.add.data    = get_permission('CENTER', 'ADD')
        form.center_permissions.update.data = get_permission('CENTER', 'UPDATE')
        form.center_permissions.delete.data = get_permission('CENTER', 'DELETE')
        form.room_permissions.add.data    = get_permission('ROOM', 'ADD')
        form.room_permissions.update.data = get_permission('ROOM', 'UPDATE')
        form.room_permissions.delete.data = get_permission('ROOM', 'DELETE')
        form.folder_permissions.add.data    = get_permission('FOLDER', 'ADD')
        form.folder_permissions.update.data = get_permission('FOLDER', 'UPDATE')
        form.folder_permissions.delete.data = get_permission('FOLDER', 'DELETE')
        form.image_permissions.add.data    = get_permission('IMAGE', 'ADD')
        form.image_permissions.update.data = get_permission('IMAGE', 'UPDATE')
        form.image_permissions.delete.data = get_permission('IMAGE', 'DELETE')
        form.repository_permissions.add.data    = get_permission('REPOSITORY', 'ADD')
        form.repository_permissions.update.data = get_permission('REPOSITORY', 'UPDATE')
        form.repository_permissions.delete.data = get_permission('REPOSITORY', 'DELETE')

    form.scopes.choices = get_available_centers()

    return render_template('auth/edit_user.html', form=form)


@app.route('/user/edit', methods=['POST'])
@handle_server_errors('users')
@login_required
def user_edit_post():
    form = EditUserForm(request.form)
    form.scopes.choices = get_center_choices()
    if not form.validate():
        flash(form.errors, category='error')
        return redirect(url_for('users'))

    username = form.username.data
    old_user_data = get_user(username)
    if not old_user_data:
        flash(_('User {} does not exist').format(username), category='error')
        return redirect(url_for('users'))

    preserve_pwd = (not form.pwd.data and not form.pwd_confirm.data)
    if preserve_pwd:
        form.pwd.data = old_user_data.get("PASS")

    if save_user(form, preserve_pwd):
        flash(_('User edited successfully'), category='info')
    return redirect(url_for('users'))


@app.route('/user/delete', methods=['GET'])
@handle_server_errors('users')
@login_required
def user_delete_get():
    username_set = parse_elements(request.args.to_dict())
    if not validate_elements(username_set, max_len=1):
        return redirect(url_for('users'))

    username = username_set.pop()
    user = get_user(username)
    if not user:
        flash(_('User {} does not exist').format(username), category='error')
        return redirect(url_for('users'))

    form = DeleteUserForm()
    form.username.data = user.get('USER')

    return render_template('auth/delete_user.html', form=form)


@app.route('/action/image/list', methods=['GET'])
@handle_server_errors('images')
@login_required
def action_image_list():
    params = request.args.to_dict()
    ids = parse_elements(params)
    ids = [int(id) for id in ids if id.isdigit()]
    if not ids:
        flash(_('Please, select one more images to be listed'), category='error')
        return redirect(url_for('images'))
    server = get_server_from_ip_port(params['image-server'])
    responses = get_images_grouped_by_repos()
    servers = []
    for server in responses:
        repos = []
        for unused, repo in server['repos'].items():
            images=[]
            for img in repo['images']:
                if int(img['id']) in ids:
                    images.append(img)
            if images:
                repos.append((repo['name'], images))
        if repos:
            s={}
            s['name'] = server['server'].name
            s['repos'] = repos
            servers.append(s)
    return render_template('actions/list_images.html',
                           servers=servers, responses=responses)

@app.route('/user/delete', methods=['POST'])
@handle_server_errors('users')
@login_required
def user_delete_post():
    form = DeleteUserForm(request.form)
    if not form.validate():
        flash(form.errors, category='error')
        return redirect(url_for('users'))

    username = form.username.data
    if not get_user(username):
        flash(_('User {} does not exist').format(username), category='error')
        return redirect(url_for('users'))

    delete_user(username)

    flash(_('User {} deleted').format(username), category='info')
    return redirect(url_for('users'))


@app.route('/action/image/info', methods=['GET'])
@handle_server_errors('images')
@login_required
def action_image_info():
    form = ImageDetailsForm()
    params = request.args.to_dict()
    ids = parse_elements(params)
    if not validate_elements(ids, max_len=1):
        return redirect(url_for('images'))

    id = ids.pop()
    server = get_server_from_ip_port(params['image-server'])
    r = server.get('/images')

    images = r.json()['images']
    image = next(img for img in images if img['id'] == int(id))

    form.name.data = image['name']
    # Bytes to Mebibytes
    form.size.data = image['size'] / 1024 ** 2
    form.datasize.data = image['datasize'] / 1024 ** 2
    form.modified.data = image['modified']
    form.permissions.data = image['permissions']
    form.software_id.data = image['software_id']
    form.description.data = image['description']

    checksum = image.get('checksum', _('Unknown'))
    if not checksum:
        checksum = _('Unknown')

    form.checksum.data = checksum

    responses = get_images_grouped_by_repos()

    r = server.get('/image/restrict', {'image': image['id']})

    form.scopes.choices = get_available_centers()
    form.scopes.data = [str(scope) for scope in r.json().get('scopes')]

    return render_template('actions/image_details.html', form=form,
                           responses=responses)

@app.route('/action/image/delete', methods=['GET', 'POST'])
@handle_server_errors('images')
@login_required
def action_image_delete():
    form = GenericForm(request.form)
    if request.method == 'POST':
        ids = form.ids.data.split(' ')
        if not validate_elements(ids):
            return redirect(url_for('images'))

        server = get_server_from_ip_port(form.server.data)

        for id in ids:
            payload = {'image': id}
            r = server.post('/image/delete', payload)

        flash(_('Image deletion request sent successfully'), category='info')
        return redirect(url_for('images'))
    else:
        params = request.args.to_dict()
        image_ids = [imgid for name, imgid in params.items()
                  if name != 'csrf_token' and name != 'image-server']

        if not validate_elements(image_ids):
            return redirect(url_for('images'))

        server = get_server_from_ip_port(params['image-server'])
        responses = get_images_grouped_by_repos()

        form.ids.data = ' '.join(image_ids)
        form.server.data = params['image-server']

        return render_template('actions/delete_image.html', form=form,
                               image_ids=image_ids,
                               responses=responses)

@app.route('/action/image/config', methods=['GET', 'POST'])
@handle_server_errors('images')
@login_required
def action_image_config():
    form = ImageConfigForm(request.form)
    if request.method == 'POST':
        image_id = int(form.image_id.data)
        server = get_server_from_ip_port(form.server.data)

        scope_list = [int(scope) for scope in form.scopes.data]

        payload = {'image': image_id, 'scopes': scope_list}

        r = server.post('/image/restrict', payload)

        flash(_('Image updated successfully'), category='info')
        return redirect(url_for('images'))
    else:
        params = request.args.to_dict()
        images = [(name, imgid) for name, imgid in params.items()
                  if name != 'csrf_token' and name != 'image-server']
        if not validate_elements(images, max_len=1):
            return redirect(url_for('images'))

        image_name, image_id = images[0]
        image_name=image_name.split('_', 1)[0]
        server = get_server_from_ip_port(params['image-server'])

        form.image_id.data = image_id
        form.name.data = image_name

        r = server.get('/image/restrict', {'image': int(image_id)})

        form.server.data = params['image-server']
        form.scopes.choices = get_available_centers()
        form.scopes.data = [str(scope) for scope in r.json().get('scopes')]

        responses = get_images_grouped_by_repos()

        return render_template('actions/image_config.html', form=form,
                               responses=responses)

@app.route('/action/log', methods=['GET'])
@login_required
def action_legacy_log():
    ips = parse_elements(request.args.to_dict())
    if not validate_elements(ips, max_len=1):
        return redirect(url_for('commands'))
    ip = ips.pop()
    log_file = Path("/opt/opengnsys/log/clients/" + str(ip) + ".log")
    if not os.access(log_file, os.R_OK):
        flash(_('No log available for this client yet'), category='error')
        return redirect(url_for('commands'))
    log = log_file.read_text()
    if log:
        scopes, clients = get_scopes(set(ips))
        return render_template('actions/legacy/log.html', log=log,
                               scopes=scopes)
    else:
        return redirect(url_for('commands'))

@app.route('/action/rt-log', methods=['GET'])
@login_required
def action_legacy_rt_log():
    ips = parse_elements(request.args.to_dict())
    if not validate_elements(ips, max_len=1):
        return redirect(url_for('commands'))
    ip = ips.pop()
    scheme = "http://"
    rt_log_path = "/cgi-bin/httpd-log.sh"
    rt_log_url = scheme + ip + rt_log_path
    return redirect(rt_log_url)

