import {Injectable, isDevMode} from '@angular/core';
import {
    EmptyDictionary,
    EntityCollections,
    EntityComparator,
    EntityDictionary,
    EntityIdType,
    EntitySortBy,
    EntityUpdate,
    KeysDictionary,
} from '@angular-ru/cdk/entity';
import {sortByAsc, sortByDesc} from '@angular-ru/cdk/object';
import {PrimaryKey, SortOrderType} from '@angular-ru/cdk/typings';
import {isNil} from '@angular-ru/cdk/utils';
import {Computed, DataAction, Payload} from '@angular-ru/ngxs/decorators';
import {ensureDataStateContext, ensureSnapshot} from '@angular-ru/ngxs/internals';
import {NGXS_DATA_EXCEPTIONS} from '@angular-ru/ngxs/tokens';
import {EntityContext, EntityRepository} from '@angular-ru/ngxs/typings';
import {ActionType, StateContext} from '@ngxs/store';
import {Observable} from 'rxjs';
import {map} from 'rxjs/operators';

import {AbstractRepository} from '../common/abstract-repository';

@Injectable()
export abstract class AbstractNgxsDataEntityCollectionsRepository<
        V,
        K extends number | string = EntityIdType,
        C = Record<string, any>,
    >
    extends AbstractRepository<EntityCollections<V, K, C>>
    implements EntityRepository<V, K, C>
{
    private readonly context!: EntityContext<V, K, C>;
    public primaryKey: string = PrimaryKey.ID;
    public comparator: EntityComparator<V> | null = null;

    @Computed()
    public get snapshot(): EntityCollections<V, K, C> {
        return ensureSnapshot(this.getState());
    }

    @Computed()
    public get ids(): K[] {
        return this.snapshot.ids;
    }

    @Computed()
    public get entities(): EntityDictionary<K, V> {
        return this.snapshot.entities;
    }

    @Computed()
    public get entitiesArray(): V[] {
        const snapshot: EntityCollections<V, K, C> = this.snapshot;

        return snapshot.ids.map((id: K): V => snapshot.entities[id]);
    }

    @Computed()
    public get ids$(): Observable<K[]> {
        return this.state$.pipe(
            map((value: EntityCollections<V, K, C>): K[] => value.ids),
        );
    }

    @Computed()
    public get entities$(): Observable<EntityDictionary<K, V>> {
        return this.state$.pipe(
            map(
                (value: EntityCollections<V, K, C>): EntityDictionary<K, V> =>
                    value.entities,
            ),
        );
    }

    @Computed()
    public get entitiesArray$(): Observable<V[]> {
        return this.state$.pipe(
            map((value: EntityCollections<V, K, C>): V[] =>
                value.ids.map((id: K): V => value.entities[id]),
            ),
        );
    }

    protected get ctx(): EntityContext<V, K, C> {
        return ensureDataStateContext<
            EntityCollections<V, K, C>,
            StateContext<EntityCollections<V, K, C>>
        >(this.context as StateContext<EntityCollections<V, K, C>>);
    }

    @DataAction()
    public reset(): void {
        this.setEntitiesState(this.initialState);
        this.markAsDirtyAfterReset();
    }

    @DataAction()
    public addOne(@Payload('entity') entity: V): void {
        this.addEntityOne(entity);
    }

    @DataAction()
    public addMany(@Payload('entities') entities: V[]): void {
        this.addEntitiesMany(entities);
    }

    @DataAction()
    public setOne(@Payload('entity') entity: V): void {
        this.setEntityOne(entity);
    }

    @DataAction()
    public setMany(@Payload('entities') entities: V[]): void {
        this.setEntitiesMany(entities);
    }

    @DataAction()
    public setAll(@Payload('entities') entities: V[]): void {
        this.setEntitiesAll(entities);
    }

    @DataAction()
    public updateOne(@Payload('update') update: EntityUpdate<V, K>): void {
        this.updateEntitiesMany([update]);
    }

    @DataAction()
    public updateMany(@Payload('updates') updates: Array<EntityUpdate<V, K>>): void {
        this.updateEntitiesMany(updates);
    }

    @DataAction()
    public upsertOne(@Payload('entity') entity: V): void {
        this.upsertEntitiesMany([entity]);
    }

    @DataAction()
    public upsertMany(@Payload('entities') entities: V[]): void {
        this.upsertEntitiesMany(entities);
    }

    @DataAction()
    public removeOne(@Payload('id') id: K): void {
        this.removeEntitiesMany([id]);
    }

    @DataAction()
    public removeMany(@Payload('ids') ids: K[]): void {
        this.removeEntitiesMany(ids);
    }

    @DataAction()
    public removeByEntity(@Payload('entity') entity: V): void {
        const id: K = this.selectId(entity);

        this.removeEntitiesMany([id]);
    }

    @DataAction()
    public removeByEntities(@Payload('entities') entities: V[]): void {
        const ids: K[] = [];

        for (const entity of entities) {
            const id: K = this.selectId(entity);

            ids.push(id);
        }

        this.removeEntitiesMany(ids);
    }

    @DataAction()
    public removeAll(): void {
        this.setAll([]);
    }

    @DataAction()
    public sort(@Payload('comparator') comparator?: EntityComparator<V>): void {
        this.comparator = comparator ?? this.comparator;

        if (isNil(this.comparator)) {
            console.warn(NGXS_DATA_EXCEPTIONS.NGXS_COMPARE);

            return;
        }

        this.setEntitiesState(this.getState());
    }

    @DataAction()
    public patchState(
        @Payload('patchValue') value: Partial<EntityCollections<V, K, C>>,
    ): void {
        this.ctx.patchState(value);
    }

    public setComparator(comparator: EntityComparator<V> | null): this {
        this.comparator = comparator;

        return this;
    }

    public dispatch(actions: ActionType | ActionType[]): Observable<void> {
        return this.ctx.dispatch(actions);
    }

    public getState(): EntityCollections<V, K, C> {
        return this.ctx.getState();
    }

    public selectId(entity: V): K {
        return (entity as any)?.[this.primaryKey];
    }

    public selectOne(id: K): V | null {
        return this.snapshot.entities[id] ?? null;
    }

    public selectAll(): V[] {
        const state: EntityCollections<V, K, C> = this.getState();

        return state.ids.map((id: K): V => state.entities[id] as V);
    }

    protected addEntityOne(entity: V): void {
        const state: EntityCollections<V, K, C> = this.getState();
        const id: K = this.selectIdValue(entity);

        if (id in state.entities) {
            return;
        }

        this.setEntitiesState({
            ...state,
            ids: [...state.ids, id],
            entities: {...state.entities, [id]: entity},
        });
    }

    protected addEntitiesMany(entities: V[]): void {
        const state: EntityCollections<V, K, C> = this.getState();
        const dictionary: EmptyDictionary<K, V> | EntityDictionary<K, V> = {};
        const ids: K[] = [];

        for (const entity of entities) {
            const id: K = this.selectIdValue(entity);

            if (id in state.entities || id in dictionary) {
                continue;
            }

            ids.push(id);
            dictionary[id] = entity;
        }

        if (ids.length > 0) {
            this.setEntitiesState({
                ...state,
                ids: [...state.ids, ...ids],
                entities: {...state.entities, ...dictionary},
            });
        }
    }

    protected setEntitiesAll(entities: V[]): void {
        const state: EntityCollections<V, K, C> = this.getState();
        const dictionary: EmptyDictionary<K, V> | EntityDictionary<K, V> = {};
        const ids: K[] = [];

        for (const entity of entities) {
            const id: K = this.selectIdValue(entity);

            if (id in dictionary) {
                continue;
            }

            ids.push(id);
            dictionary[id] = entity;
        }

        this.setEntitiesState({
            ...state,
            ids,
            entities: dictionary as EntityDictionary<K, V>,
        });
    }

    protected setEntityOne(entity: V): void {
        const state: EntityCollections<V, K, C> = this.getState();
        const id: K = this.selectIdValue(entity);

        if (id in state.entities) {
            this.setEntitiesState({
                ...state,
                entities: {...state.entities, [id]: entity},
            });
        } else {
            this.setEntitiesState({
                ...state,
                ids: [...state.ids, id],
                entities: {...state.entities, [id]: entity},
            });
        }
    }

    protected setEntitiesMany(entities: V[]): void {
        for (const entity of entities) {
            this.setEntityOne(entity);
        }
    }

    // eslint-disable-next-line max-lines-per-function
    protected updateEntitiesMany(updates: Array<EntityUpdate<V, K>>): void {
        const state: EntityCollections<V, K, C> = this.getState();
        const newUpdates: Array<EntityUpdate<V, K>> = updates.filter(
            (update: EntityUpdate<V, K>): boolean => update.id in state.entities,
        );

        if (newUpdates.length === 0) {
            return;
        }

        const keys: KeysDictionary<K> = this.generateKeyMap(state);
        const entities: EntityDictionary<K, V> = {...state.entities};

        for (const update of newUpdates) {
            const updated: V = this.updateOrigin(entities, update);
            const newId: K = this.selectIdValue(updated);

            if (newId !== update.id) {
                delete keys[update.id];
                delete entities[update.id];
            }

            keys[update.id] = newId;
            entities[newId] = updated;
        }

        this.setEntitiesState({
            ...state,
            ids: state.ids.map((id: K): K => keys[id] ?? id),
            entities,
        });
    }

    protected upsertEntitiesMany(entities: V[]): void {
        const state: EntityCollections<V, K, C> = this.getState();
        const updates: Array<EntityUpdate<V, K>> = [];
        const added: V[] = [];

        for (const entity of entities) {
            const id: K = this.selectIdValue(entity);

            if (id in state.entities) {
                updates.push({id, changes: entity});
            } else {
                added.push(entity);
            }
        }

        this.updateMany(updates);
        this.addMany(added);
    }

    protected removeEntitiesMany(ids: K[]): void {
        const state: EntityCollections<V, K, C> = this.getState();
        const keys: KeysDictionary<K> = this.generateKeyMap(state);
        const entities: EntityDictionary<K, V> = {...state.entities};

        for (const id of ids) {
            if (id in entities) {
                delete keys[id];
                delete entities[id];
            }
        }

        this.setEntitiesState({
            ...state,
            ids: state.ids.filter((id: K): boolean => id in keys),
            entities,
        });
    }

    protected setEntitiesState(state: EntityCollections<V, K, C>): void {
        const ids: K[] = this.sortKeysByComparator(state.ids, state.entities);

        this.ctx.patchState({...state, ids, entities: state.entities});
    }

    protected sortKeysByComparator(
        originalIds: K[],
        entities: EntityDictionary<K, V>,
    ): K[] {
        if (isNil(this.comparator)) {
            return originalIds;
        }

        const ids: K[] = originalIds.slice();
        const comparator: EntityComparator<V> = this.comparator;

        if (typeof comparator === 'function') {
            return ids.sort((a: K, b: K): number =>
                comparator(entities[a] as V, entities[b] as V),
            );
        }

        return this.sortByComparatorOptions(ids, comparator, entities);
    }

    private sortByComparatorOptions(
        ids: K[],
        comparator: EntitySortBy<V>,
        entities: EntityDictionary<K, V>,
    ): K[] {
        switch (comparator?.sortByOrder) {
            case SortOrderType.ASC:
                return ids.sort((a: K, b: K): number =>
                    sortByAsc(comparator?.sortBy, entities[a] as V, entities[b] as V),
                );

            case SortOrderType.DESC:
                return ids.sort((a: K, b: K): number =>
                    sortByDesc(comparator?.sortBy, entities[a] as V, entities[b] as V),
                );

            default:
                if (isDevMode()) {
                    console.warn(
                        `Invalid --> { sortByOrder: "${comparator?.sortByOrder}" } not supported!`,
                    );
                }

                return ids;
        }
    }

    private generateKeyMap(state: EntityCollections<V, K, C>): KeysDictionary<K> {
        return state.ids.reduce(
            (keyDictionary: KeysDictionary<K>, id: K): KeysDictionary<K> => {
                keyDictionary[id] = id;

                return keyDictionary;
            },
            {},
        );
    }

    private updateOrigin(
        entities: EntityDictionary<K, V>,
        update: EntityUpdate<V, K>,
    ): V {
        const original: V | undefined = entities[update.id];

        return {...original, ...update.changes} as V;
    }

    private selectIdValue(entity: V): K {
        const id: K = this.selectId(entity);
        const invalidId: boolean = isNil(id) && isDevMode();

        if (invalidId) {
            console.warn(
                `The entity passed to the 'selectId' implementation returned ${id}.`,
                "You should probably provide your own 'selectId' implementation.",
                'The entity that was passed:',
                entity,
                'The current `selectId` implementation: (entity: V): K => entity.id',
            );
        }

        return id;
    }
}
