import { BuildDescriptorFn, IDescriptorBuilder, DepsMap, Resolve, DescriptorMap } from "./interfaces";
import { Descriptor, ILifetime, ActivationType } from "./interfaces";
import { DescriptorImpl } from "./DescriptorImpl";
import { contextLifetime, emptyLifetime, hierarchyLifetime, LifetimeManager, singletonLifetime } from "./LifetimeManager";
import { each, isPromise, isString, key, oid } from "./traits";

/**
 * @template {S} Карта доступных зависимостей, как правило `ContainerServices`
 * @template {T} Тип сервиса
 */
export class DescriptorBuilder<S, T, R, U extends keyof S> implements IDescriptorBuilder<S, T, R, U> {
    private readonly _lifetimeManager: LifetimeManager;
    private readonly _cb: (d: Descriptor<S, T>) => void;

    private readonly _eb: (err: unknown) => void;

    private readonly _refs: DepsMap<R>;

    private _lifetime = emptyLifetime<T>();

    private _overrides: DescriptorMap<S>;

    private _cleanup?: (item: T) => void;

    private _factory?: (refs: R) => NonNullable<T>;

    private _pending = 1;

    private _failed = false;

    private _finalized = false;

    /** 
     * Creates new DescriptorBuilder. Accepts a lifetime container for resolving "container"
     * lifetime.
     * 
     * @param lifetimeManager The lifetime container is the container where the service is to be registered.
     * @param cb The callback to receive the built service descriptor
     * @param eb The callback to receive the error due 
     */
    constructor(lifetimeManager: LifetimeManager, cb: (d: Descriptor<S, T>) => void, eb: (err: unknown) => void) {
        this._lifetimeManager = lifetimeManager;
        this._cb = cb;
        this._eb = eb;
        this._overrides = {};
        this._refs = {};
    }

    /** Declares dependencies to be consumed in the factory method */
    wants<X extends DepsMap<S> & Record<keyof R & keyof X, never>>(refs: X):
        IDescriptorBuilder<S, T, R & { [k in keyof X]: Resolve<S, X[k]>; }, U> {

        each(refs, (v, k) => this._refs[k] = v);

        return this as IDescriptorBuilder<S, T, R & {
            [k in keyof X]: Resolve<S, X[k]>;
        }, U>;
    }

    /** Registers a factory method for the service */
    factory(f: (refs: R) => NonNullable<T>): void {
        this._assertBuilding();
        this._factory = f;
        this._finalize();
        this._complete();
    }


    private _assertBuilding() {
        if (this._finalized)
            throw new Error("The descriptor builder is finalized");
    }

    private _finalize() {
        this._finalized = true;
    }

    override<K extends U>(name: K, builder: BuildDescriptorFn<S, NonNullable<S[K]>, U>): this;
    override<K extends U>(services: { [k in K]: BuildDescriptorFn<S, NonNullable<S[k]>, U> }): this;
    override<K extends U>(nameOrServices: K | { [name in K]: BuildDescriptorFn<S, NonNullable<S[K]>, U> }, builder?: BuildDescriptorFn<S, NonNullable<S[K]>, U>): this {
        this._assertBuilding();
        const guard = (v: void | Promise<void>) => {
            if (isPromise(v))
                v.catch(err => this._fail(err));
        };

        if (typeof nameOrServices !== "object") {
            if (builder) {
                this._defer();
                const d = new DescriptorBuilder<S, NonNullable<S[K]>, object, U>(
                    this._lifetimeManager,
                    result => {
                        this._overrides[nameOrServices] = result;
                        this._complete();
                    },
                    err => this._fail(err)
                );

                try {
                    guard(builder(d));
                } catch (err) {
                    this._fail(err);
                }
            }
        } else {
            each(nameOrServices, (v, k) => this.override(k, v));
        }
        return this;
    }

    /** Specified the singleton lifetime for the service */
    lifetime(lifetime: "singleton", typeId: string): this;
    /**
     * Specifies the lifetime for the service, either {@linkcode ILifetime<T>}
     * object or {@linkcode ActivationType} literal.
     * @param lifetime 
     */
    lifetime(lifetime: ILifetime<T> | Exclude<ActivationType, "singleton">): this;
    lifetime(lifetime: ILifetime<T> | ActivationType, typeId?: string): this {
        this._assertBuilding();
        if (isString(lifetime)) {
            this._lifetime = this._resolveLifetime(lifetime, typeId);
        } else {
            this._lifetime = lifetime;
        }
        return this;
    }

    /** Registers cleanup callback, used when lifetime of the instance is managed
     * by the container or some external mechanism
     */
    cleanup(cb: (item: T) => void): this {
        this._assertBuilding();
        this._cleanup = cb;
        return this;
    }

    /** Registers a value as the instance of the service */
    value(v: NonNullable<T>): void {
        this._assertBuilding();
        this._cb({
            activate() {
                return v;
            }
        });
        this._finalize();
    }

    _resolveLifetime<T>(activation: ActivationType, typeId?: string | object): ILifetime<T> {
        switch (activation) {
            case "container":
                return this._lifetimeManager.create();
            case "hierarchy":
                return hierarchyLifetime();
            case "context":
                return contextLifetime();
            case "singleton": {
                if (!typeId)
                    throw Error("The singleton activation requires a typeId");
                const _oid = isString(typeId) ? typeId : oid(typeId);
                return singletonLifetime(_oid);
            }
            default:
                return emptyLifetime();
        }
    }

    _defer() {
        this._pending++;
    }

    _complete() {
        if (--this._pending === 0) {
            if (!this._factory)
                throw new Error("The factory must be specified");

            this._cb(new DescriptorImpl<S, T>({
                lifetime: this._lifetime,
                factory: this._factory as (refs: Record<key, unknown>) => NonNullable<T>,
                overrides: this._overrides,
                cleanup: this._cleanup
            }));
        }
    }

    _fail(err: unknown) {
        if (!this._failed) {
            this._failed = true;
            this._eb.call(undefined, err);
        }
    }

}
