function assert(condition: boolean) {
    if (!condition) throw new Error();
}

type ServiceName = string;
type InterfaceName = string;

export interface Service {
    readonly name: ServiceName;
    readonly implements?: InterfaceName[];
    readonly requires: (ServiceName | InterfaceName)[];
}

export interface ResolvedService {
    readonly name: ServiceName;
    readonly requires: ServiceName[];
}

interface Edge {
    readonly service: Node;
    readonly dependency: Node;
}

class Node {
    status: 'required' | 'problem' | 'maybe' | 'possible' = 'maybe';

    constructor(
        private readonly g: Graph,
        readonly name: ServiceName | InterfaceName,
        readonly type: 'service' | 'interface'
    ) {}

    getDependencies(): Node[] {
        return this.g.getEdgesWhereServiceIs(this.name).map(edge => edge.dependency);
    }
    getDependants(): Node[] {
        return this.g.getEdgesWhereDependencyIs(this.name).map(edge => edge.service);
    }
    addDependency(n: Node): void {
        this.g.addEdge(this, n);
    }
}

class Graph {
    private readonly nodes = new Map<ServiceName | InterfaceName, Node>();
    private readonly edges: Edge[] = [];

    public getNode(name: ServiceName): Node {
        return this.nodes.get(name);
    }

    __debug() {
        console.log(Array.from(this.nodes.values()).map(s => ({name: s.name, status: s.status})));
    }

    public getOrCreateNode(name: ServiceName, type: 'service' | 'interface'): Node {
        let node = this.nodes.get(name);
        if (node) {
            if (node.type !== type) {
                throw new Error(`${name} can not be a service and an interface`);
            }
            return node;
        }
        node = new Node(this, name, type);
        this.nodes.set(name, node);
        return node;
    }

    public addEdge(from: Node, to: Node) {
        let edge = this.edges.find(
            e => e.service.name === from.name && e.dependency.name === to.name
        );
        if (!edge) {
            edge = {service: from, dependency: to};
            this.edges.push(edge);
        }
    }

    public getEdgesWhereServiceIs(service: ServiceName): Edge[] {
        return this.edges.filter(e => e.service.name === service);
    }

    public getEdgesWhereDependencyIs(dependency: ServiceName): Edge[] {
        return this.edges.filter(e => e.dependency.name === dependency);
    }

    public setServiceStatus(nameOrNode: ServiceName | Node, newStatus: Node['status']) {
        const node = typeof nameOrNode === 'string' ? this.nodes.get(nameOrNode) : nameOrNode;
        if (node.status === newStatus) {
            return;
        }
        node.status = newStatus;
        node.getDependants().forEach(dep => {
            this.updateService(dep);
        });
        node.getDependencies().forEach(dep => {
            this.updateService(dep);
        });
    }

    private updateService(node: Node) {
        let newStatus: Node['status'] = node.status;
        if (node.type === 'service') {
            if (node.status !== 'required') {
                if (node.getDependants().some(d => d.status === 'required')) {
                    newStatus = 'required';
                } else {
                    if (
                        node
                            .getDependencies()
                            .every(({status}) => status === 'required' || status === 'possible')
                    ) {
                        newStatus = 'possible';
                    }
                }
            }
        } else if (node.type === 'interface') {
            const possibleImplementations = node
                .getDependencies()
                .map(dep => dep.status)
                .filter(status => status === 'required' || status === 'possible');
            if (possibleImplementations.length === 1) {
                newStatus = 'possible';
            } else if (
                possibleImplementations.filter(status => status === 'required').length === 1
            ) {
                newStatus = 'possible';
            } else {
                newStatus = 'problem';
            }
        } else {
            assert(false);
        }
        if (node.status !== newStatus) {
            this.setServiceStatus(node, newStatus);
        }
    }
}

export function resolve(rootService: ServiceName, availableServices: Service[]): ResolvedService[] {
    // Create Maps for fast look-ups
    const interfaces = new Map<InterfaceName, ServiceName[]>();
    const services = new Map<ServiceName, Service>();

    for (const service of availableServices) {
        (service.implements || []).forEach(interfaceName => {
            const arr = interfaces.get(interfaceName) || [];
            arr.push(service.name);
            interfaces.set(interfaceName, arr);
        });
        services.set(service.name, service);
    }

    const g = new Graph();

    // Traverse services creating the graph structure
    const alreadyTraversed = new Set<ServiceName>();
    (function traverse(service: ServiceName) {
        if (alreadyTraversed.has(service)) return;
        alreadyTraversed.add(service);

        const serviceNode = g.getOrCreateNode(service, 'service');

        services.get(service).requires.forEach(serviceDependency => {
            if (services.has(serviceDependency)) {
                serviceNode.addDependency(g.getOrCreateNode(serviceDependency, 'service'));
                traverse(serviceDependency);
                return;
            }
            const possibleImplementations = interfaces.get(serviceDependency) || [];
            if (possibleImplementations.length === 0) {
                throw new Error('Unable to find any implementations of interface');
            }
            const interfaceNode = g.getOrCreateNode(serviceDependency, 'interface');
            serviceNode.addDependency(interfaceNode);

            possibleImplementations.forEach(possibleImpl => {
                interfaceNode.addDependency(g.getOrCreateNode(possibleImpl, 'service'));
                traverse(possibleImpl);
            });
        });
    })(rootService);

    g.setServiceStatus(rootService, 'required');

    // now should be able to walk graph and encounter no 'problems' if all is well

    const resolvedServices = new Map<ServiceName, ResolvedService>();

    (function traverse(serviceName: ServiceName): void {
        if (resolvedServices.has(serviceName)) {
            return;
        }
        const resolvedService: ResolvedService = {
            name: serviceName,
            requires: []
        };
        resolvedServices.set(serviceName, resolvedService);
        const serviceReqs = services.get(serviceName).requires;
        serviceReqs.forEach(depName => {
            const node = g.getNode(depName);
            if (node.type === 'service') {
                assert(node.status !== 'problem');
                resolvedService.requires.push(node.name);
                traverse(node.name);
            } else if (node.type === 'interface') {
                assert(node.status !== 'problem');
                const implementations = node
                    .getDependencies()
                    .filter(({status}) => status === 'required' || status === 'possible');
                assert(implementations.length > 0);
                if (implementations.length === 1) {
                    resolvedService.requires.push(implementations[0].name);
                    traverse(implementations[0].name);
                } else {
                    const solved = implementations.filter(n => n.status === 'required');
                    assert(solved.length === 1);
                    resolvedService.requires.push(solved[0].name);
                    traverse(solved[0].name);
                }
            } else {
                assert(false);
            }
        });
        assert(resolvedService.requires.length === serviceReqs.length);
    })(rootService);

    return Array.from(resolvedServices.values()).sort((a, b) => a.name.localeCompare(b.name));
}