"""Bound private network commands and serialize only this tooling's allocation window."""
from contextlib import contextmanager
import fcntl
import os
import stat
import time

from identity_diagnostics import ReaderFailure, bounded

LOCK = '/run/lock/gowalk-backend-networks.lock'


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


def run(argv, seconds=10):
    try:
        return bounded(argv, seconds=seconds)
    except (ReaderFailure, OSError):
        raise NetworkFailure('network_command_unavailable') from None


@contextmanager
def allocation_lock(path=LOCK, seconds=15):
    flags = os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW | os.O_CLOEXEC
    try:
        descriptor = os.open(str(path), flags, 0o600)
    except OSError:
        raise NetworkFailure('allocation_lock_unavailable') from None
    try:
        observed = os.fstat(descriptor)
        if not stat.S_ISREG(observed.st_mode) or observed.st_uid != os.geteuid() or observed.st_mode & 0o077:
            raise NetworkFailure('allocation_lock_unavailable')
        deadline = time.monotonic() + seconds
        while True:
            try:
                fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
                break
            except BlockingIOError:
                if time.monotonic() >= deadline:
                    raise NetworkFailure('allocation_lock_busy') from None
                time.sleep(0.05)
        if os.stat(str(path), follow_symlinks=False).st_ino != observed.st_ino:
            raise NetworkFailure('allocation_lock_unavailable')
        yield
    finally:
        os.close(descriptor)


def compose_configuration(app, env_files):
    import json
    args = ['docker', 'compose']
    for value in env_files:
        args.extend(['--env-file', value])
    args.extend(['--project-name', app, 'config', '--format', 'json'])
    try:
        return json.loads(run(args, seconds=30))
    except (ValueError, TypeError):
        raise NetworkFailure('compose_configuration_unavailable') from None
