"""Select only ordinary, automatically named Compose networks; never rewrite authored IPAM."""
import hashlib
import ipaddress
import json
import re

OWNER = 'com.gowalk.cicd.network-contract'
SCHEMA = 'com.gowalk.cicd.network-schema'
PROJECT = 'com.docker.compose.project'
NETWORK = 'com.docker.compose.network'


class ContractFailure(Exception):
    def __init__(self, code):
        self.code = code
        super().__init__(code)


def identity(value, pattern):
    if not isinstance(value, str) or not re.fullmatch(pattern, value):
        raise ContractFailure('invalid_project_contract')
    return value


def disposition(key, value, project, services):
    if value.get('external') is True:
        return 'external'
    if value.get('name') != project + '_' + key:
        return 'custom_name'
    if value.get('ipam'):
        return 'authored_ipam'
    allowed = {'name', 'driver', 'internal', 'attachable', 'enable_ipv4', 'enable_ipv6', 'labels', 'ipam'}
    if set(value) - allowed or value.get('driver', 'bridge') not in ('', 'bridge'):
        return 'custom_settings'
    if value.get('enable_ipv6') or value.get('enable_ipv4') is False:
        return 'address_family'
    for service in services.values():
        options = (service.get('networks') or {}).get(key) or {}
        if options.get('ipv4_address') or options.get('ipv6_address'):
            return 'static_address'
    return 'eligible'


def plans(config, project):
    identity(project, r'[a-z0-9][a-z0-9-]{0,79}')
    if not isinstance(config, dict) or config.get('name') != project:
        raise ContractFailure('invalid_project_contract')
    services, networks = config.get('services', {}), config.get('networks', {})
    if not isinstance(services, dict) or not isinstance(networks, dict) or len(networks) > 64:
        raise ContractFailure('invalid_project_contract')
    result = []
    for key, value in sorted(networks.items()):
        identity(key, r'[A-Za-z0-9][A-Za-z0-9_.-]{0,127}')
        if not isinstance(value, dict) or not isinstance(value.get('name'), str):
            raise ContractFailure('invalid_project_contract')
        selected = disposition(key, value, project, services)
        labels = value.get('labels', {})
        if selected == 'eligible':
            if not isinstance(labels, dict) or any(not isinstance(v, str) for v in labels.values()):
                raise ContractFailure('invalid_project_contract')
            if any(k.startswith('com.docker.compose.') or k.startswith('com.gowalk.cicd.') for k in labels):
                raise ContractFailure('reserved_network_label')
        canonical = json.dumps(value, sort_keys=True, separators=(',', ':'), ensure_ascii=True)
        result.append({'key': key, 'name': value['name'], 'project': project, 'disposition': selected,
                       'contract': hashlib.sha256(canonical.encode()).hexdigest(), 'settings': value})
    return result


def existing(plan, networks):
    matches = [item for item in networks if item['Name'] == plan['name']]
    if len(matches) > 1:
        raise ContractFailure('network_name_ambiguous')
    if not matches:
        return None
    network = matches[0]
    labels = network.get('Labels') or {}
    if plan['disposition'] in ('external', 'custom_name') and not labels.get(OWNER):
        return network
    if labels.get(PROJECT) != plan['project'] or labels.get(NETWORK) != plan['key']:
        raise ContractFailure('network_ownership_mismatch')
    if labels.get(OWNER) and (labels[OWNER] != plan['contract'] or labels.get(SCHEMA) != '1'):
        raise ContractFailure('network_contract_changed')
    return network


def create_arguments(plan, subnet, compose_version):
    value = plan['settings']
    labels = dict(value.get('labels') or {})
    labels.update({PROJECT: plan['project'], NETWORK: plan['key'], 'com.docker.compose.version': compose_version,
                   OWNER: plan['contract'], SCHEMA: '1'})
    gateway = str(ipaddress.IPv4Network(subnet).network_address + 1)
    args = ['docker', 'network', 'create', '--driver', 'bridge', '--subnet', subnet, '--gateway', gateway]
    for flag in ('internal', 'attachable'):
        if value.get(flag):
            args.append('--' + flag)
    for key, value in sorted(labels.items()):
        args.extend(['--label', key + '=' + value])
    return args + [plan['name']]


def verify_created(plan, network, subnet):
    value, labels = plan['settings'], network.get('Labels') or {}
    if (network['Name'] != plan['name'] or labels.get(OWNER) != plan['contract']
            or labels.get(SCHEMA) != '1' or labels.get(PROJECT) != plan['project']
            or labels.get(NETWORK) != plan['key'] or network.get('Driver') != 'bridge'
            or network.get('EnableIPv6') or network.get('Scope') != 'local'
            or labels.get('com.docker.compose.config-hash')):
        raise ContractFailure('created_network_unverified')
    if any(bool(network.get(k.title())) != bool(value.get(k)) for k in ('internal', 'attachable')):
        raise ContractFailure('created_network_unverified')
    pools = (network.get('IPAM') or {}).get('Config') or []
    if len(pools) != 1 or pools[0].get('Subnet') != subnet:
        raise ContractFailure('created_network_unverified')
    if (network['IPAM'].get('Driver') != 'default' or network['IPAM'].get('Options')
            or set(pools[0]) - {'Subnet', 'Gateway'}
            or pools[0].get('Gateway') != str(ipaddress.IPv4Network(subnet).network_address + 1)):
        raise ContractFailure('created_network_unverified')
    if any(labels.get(k) != v for k, v in (value.get('labels') or {}).items()):
        raise ContractFailure('created_network_unverified')
