import { inject, Injectable } from '@angular/core';
import { HttpClient, HttpResponse } from '@angular/common/http';
import { Observable, map } from 'rxjs';
import { DATE_FORMAT } from 'app/config/input.constants';
import dayjs from 'dayjs/esm';

import { isPresent } from 'app/core/util/operators';
import { ApplicationConfigService } from 'app/core/config/application-config.service';
import { createRequestOption } from 'app/core/request/request-util';

import { IResilientEntity } from './resilient-base.model';

export type PartialUpdateEntity<IE extends IResilientEntity> = Partial<IE> & Pick<IE, 'id'>;
export type EntityResponseType<IE extends IResilientEntity> = HttpResponse<IE>
export type EntityArrayResponseType<IE extends IResilientEntity> = HttpResponse<IE[]>;

/**
 * Needed type to change DATE properties into STRING properties.
 * How it works:
 *    #1. Loops through each key (K) in T.
 *    #2. If T[K] is of type Date, replace it with string | null.
 *    #3. Otherwise, keep the original type.
 */
export type RestOf<T> = {
  [K in keyof T]: T[K] extends Date ? string | null : T[K];
};

export enum DateTypeEnum {
  DATE = "DATE",            /* Date only     : yyyy-MM-dd            */
  TIMESTAMP = "TIMESTAMP"   /* Date and Time : yyyy-MM-ddTHH:mm:ss.s */
}

/**
 * Base Resiliente Service, with common methods:
 * <ul>
 *   <li>create - REST POST call to endpoint: <base_uri>/""</li>
 *   <li>UPDATE - REST PUT  call to endpoint: <base_uri>/{id}</li>
 * </ul> 
 * @param <IE> Entity domain interface (Ex. Period)
 * @param <NE> Entity domain new interface (Ex. NewPeriod)
 * @param <PE> Entity domain partial interface (Ex. PartialUpdatePeriod)
 * @class
 */
@Injectable({ providedIn: 'root' })
export abstract class ResilientService <IE extends IResilientEntity, NE> {
  protected http = inject(HttpClient);
  protected applicationConfigService = inject(ApplicationConfigService);

  /**
   * Overridable method to provide the resource main URI. Something like : 'api/entities'.
   * Implementation sample:
   * <pre>
   * private readonly resourceUrl = this.applicationConfigService.getEndpointFor('api/entities');
   * override getResourceUrl(): string { return this.resourceUrl; }
   * </pre>
   */
  protected abstract getResourceUrl(): string;

  /**
   * Overridable method to provide a list of DATE properties in <IE> entity type.
   * Will be used to auto-convert DATE properties to and from JSON string.
   * 
   * <pre>
   * private readonly dateProperties: Map<string, string> = new Map([
   *   ["propertyDate", DateTypeEnum.DATE],
   *   ["propertyTimestamp", DateTypeEnum.TIMESTAMP]
   * ]);
   * override getDateProperties(): Map<string, string> { return this.dateProperties; }
   * </pre>
   */
  protected getDateProperties(): Map<string, string> {
    return new Map<string, string>();
  }

  private needsDataConvertion(): Boolean {
    return this.getDateProperties().size>0;
  }

  create(newEntity: NE): Observable<EntityResponseType<IE>> {
    if (this.needsDataConvertion()) {
      // Convert date properties
      const convertedEntity = this.convertDateFromClient(newEntity);
      return this.http
        .post<RestOf<IE>>(this.getResourceUrl(), convertedEntity, { observe: 'response' })
        .pipe(map(res => this.convertResponseFromServer(res)));
    } else {
      // No convertion is needed
      return this.http.post<IE>(this.getResourceUrl(), newEntity, { observe: 'response' });
    }
  }

  update(entity: IE): Observable<EntityResponseType<IE>> {
    if (this.needsDataConvertion()) {
      // Convert date properties
      const copy = this.convertDateFromClient(entity);
      return this.http
        .put<RestOf<IE>>(`${this.getResourceUrl()}/${this.getEntityIdentifier(entity)}`, copy, { observe: 'response' })
        .pipe(map(res => this.convertResponseFromServer(res)));
    } else {
      // No convertion is needed
      return this.http.put<IE>(`${this.getResourceUrl()}/${this.getEntityIdentifier(entity)}`, entity, {
        observe: 'response',
      });
    }
  }

  find(id: number): Observable<EntityResponseType<IE>> {
    if (this.needsDataConvertion()) {
      // Convert date properties
      return this.http
        .get<RestOf<IE>>(`${this.getResourceUrl()}/${id}`, { observe: 'response' })
        .pipe(map(res => this.convertResponseFromServer(res)));
    } else {
      // No convertion is needed
      return this.http.get<IE>(`${this.getResourceUrl()}/${id}`, { observe: 'response' });
    }
  }

  query(req?: any): Observable<EntityArrayResponseType<IE>> {
    const options = createRequestOption(req);
    
    if (this.needsDataConvertion()) {
      // Convert date properties
      return this.http
        .get<RestOf<IE>[]>(this.getResourceUrl(), { params: options, observe: 'response' })
        .pipe(map(res => this.convertResponseArrayFromServer(res)));
    } else {
      // No convertion is needed
      return this.http.get<IE[]>(this.getResourceUrl(), { params: options, observe: 'response' });
    }
  }

  queryByCriteria(req?: any): Observable<EntityArrayResponseType<IE>> {
    const options = createRequestOption(req);
    
    if (this.needsDataConvertion()) {
      // Convert date properties
      return this.http
        .get<RestOf<IE>[]>(`${this.getResourceUrl()}/criteria`, { params: options, observe: 'response' })
        .pipe(map(res => this.convertResponseArrayFromServer(res)));
    } else {
      // No convertion is needed
      return this.http.get<IE[]>(`${this.getResourceUrl()}/criteria`, { params: options, observe: 'response' });
    }
  }
  
  delete(id: number): Observable<HttpResponse<{}>> {
    return this.http.delete(`${this.getResourceUrl()}/${id}`, { observe: 'response' });
  }

  partialUpdate(entity: PartialUpdateEntity<IE>): Observable<EntityResponseType<IE>> {
    return this.http.patch<IE>(`${this.getResourceUrl()}/${this.getEntityIdentifier(entity)}`, entity, {
      observe: 'response',
    });
  }

  getEntityIdentifier(entity: Pick<IE, 'id'>): number {
    return entity.id;
  }

  compareEntity(o1: Pick<IE, 'id'> | null, o2: Pick<IE, 'id'> | null): boolean {
    return o1 && o2 ? this.getEntityIdentifier(o1) === this.getEntityIdentifier(o2) : o1 === o2;
  }

  addEntityToCollectionIfMissing<Type extends Pick<IE, 'id'>>(
    entityCollection: Type[],
    ...entitiesToCheck: (Type | null | undefined)[]
  ): Type[] {
    const entities: Type[] = entitiesToCheck.filter(isPresent);
    if (entities.length > 0) {
      const entityCollectionIdentifiers = entityCollection.map(entityItem =>
        this.getEntityIdentifier(entityItem),
      );
      const entitiesToAdd = entities.filter(entityItem => {
        const entityIdentifier = this.getEntityIdentifier(entityItem);
        if (entityCollectionIdentifiers.includes(entityIdentifier)) {
          return false;
        }
        entityCollectionIdentifiers.push(entityIdentifier);
        return true;
      });
      return [...entitiesToAdd, ...entityCollection];
    }
    return entityCollection;
  }
  
  /* DATE Convertion methods */
  
  /*
   * ORIGINAL method code generated by JHipster, for future reference :
  protected convertDateFromClient<T extends IEntity | NewEntity | PartialUpdateEntity>(entity: T): RestOf<T> {
    const result: any = {
      ...entity,
      propertyDate: entity.propertyDate?.format(DATE_FORMAT) ?? null,
      propertyTimestamp: entity.propertyTimestamp?.toJSON() ?? null,
    };
    
    return result;
  }
  */
  protected convertDateFromClient<T extends IE | NE | PartialUpdateEntity<IE>>(entity: T): RestOf<T> {
    const result: Partial<T> = { ...entity };

    for (const key in entity) {
      //Search the entity property in the date collection
      const type = this.getDateProperties().get(key);
      
      //If found, convert (Object into String)
      if (type !== undefined) {
        //Get the value from entity instance
        const value = entity[key];
        
        if (!value) {
          (result as any)[key] = null;
          continue;
        }
        
        // Must be of type Dayjs (JHipster always defines date properties as Dayjs)
        if (!(dayjs.isDayjs(value))) {
          continue;
        }
        
        // Check if it's a Date or a DayJS type
        if (type === DateTypeEnum.TIMESTAMP) {
          (result as any)[key] = value.toJSON();
        } else if (type === DateTypeEnum.DATE) {
          (result as any)[key] = value.format(DATE_FORMAT);
        } else {
          // ERROR. This is invalid option
          console.error("❌ ERROR: Invalid DateProperty type. Valid types are: DATE | TIMESTAMP. But the type is '" + type + "'");
          throw new Error("❌ ERROR: Invalid DateProperty type. Valid types are: DATE | TIMESTAMP. But the type is '" + type + "'");
        }
      } 
    }

    return result as RestOf<T>;;
  }
  
  /*
   * ORIGINAL method code generated by JHipster, for future reference :
  protected convertDateFromServer(restEntity: RestEntity): IEntity {
    const result: any = {
      ...restEntity,
      propertyDate: restEntity.propertyDate ? dayjs(restEntity.propertyDate) : undefined,
      propertyTimestamp: restEntity.propertyTimestamp ? dayjs(restEntity.propertyTimestamp) : undefined,
    };
      
    return result;
  }
  */
  
  protected convertDateFromServer(restEntity: RestOf<IE>): IE {
    const result: Partial<RestOf<IE>> = { ...restEntity };
    
    for (const key in restEntity) {
      //Search the entity property in the date collection
      const type = this.getDateProperties().get(key);
      
      //If found, convert (String into Object)
      if (type !== undefined) {
        //Get the value from restEntity instance
        const value = restEntity[key as keyof RestOf<IE>];
        
        if (!value) { 
          (result as any)[key] = undefined;
          continue; 
        }
      
        // Create a Dayjs instance. No matter if its DATE or TIMESTAMP
        if (value && (typeof value === "string" || typeof value === "number" || value instanceof Date)) {
          (result as any)[key] = dayjs(value);
        }
      }
    }
    
    return result as IE;
  }
  
  protected convertResponseFromServer(res: HttpResponse<RestOf<IE>>): HttpResponse<IE> {
    return res.clone({
      body: res.body ? this.convertDateFromServer(res.body) : null,
    });
  }

  protected convertResponseArrayFromServer(res: HttpResponse<RestOf<IE>[]>): HttpResponse<IE[]> {
    return res.clone({
      body: res.body ? res.body.map(item => this.convertDateFromServer(item)) : null,
    });
  }

  /* Action Permission methods */
  /**
   * Method that evaluates if the @Input() {entity} can be created. This must answer the question:
   * - The entity state (or any other business rule) allows the creation ?
   * 
   * @returns true, if the entity can be created. false, otherwise.
   */
  canCreate(): boolean { return true; }

  /**
   * Method that evaluates if the @Input() {entity} can be read. This must answer the question:
   * - The entity state (or any other business rule) allows the entity to be read ?
   * 
   * @template IE - The entity domain type to evaluate.
   * @param entity - The entity instance to evaluate
   * @returns true, if the entity can be read. false, otherwise.
   */
  canRead(entity: IE): boolean { return true; }
  
  /**
   * Method that evaluates if the @Input() {entity} can be updated. This must answer the question:
   * - The entity state (or any other business rule) allows the entity to be updated ?
   * 
   * @template IE - The entity domain type to evaluate.
   * @param entity - The entity instance to evaluate
   * @returns true, if the entity can be deleted. false, otherwise.
   */
  canUpdate(entity: IE): boolean { return true; }

  /**
   * Method that evaluates if the @Input() {entity} can be deleted. This must answer the questions:
   * - The entity state (or any other business rule) allows the entity to be deleted ?
   * 
   * @template IE - The entity domain type to evaluate.
   * @param entity - The entity instance to evaluate
   * @returns true, if the entity can be deleted. false, otherwise.
   */
  canDelete(entity: IE): boolean { return true; }
}
