import { Entity as EntityServer, EntityInvalidDataError } from '@nymphjs/nymph';
import { Entity } from '@nymphjs/client';

import { HttpError } from './HttpError.js';

export type EmployeeBaseData<T> = {
  name?: string;
  id?: number;
  title?: string;
  department?: string;
  subordinates?: T[];
  salary?: number;
  current?: boolean;
  startDate?: number;
  endDate?: number;
  phone?: string;

  building?: string;

  thing?: string;
  other?: string;
};

export type EmployeeModelData = EmployeeBaseData<EmployeeModel>;

const IS_MANAGER = true;

/**
 * This class is a test class that extends the Entity class.
 */
export class EmployeeModel extends EntityServer<EmployeeModelData> {
  static ETYPE = 'employee';
  static class = 'Employee';

  protected $clientEnabledMethods = [
    '$testMethodStateless',
    '$testMethod',
    '$throwError',
    '$throwHttpError',
    '$throwHttpErrorWithDescription',
  ];
  public static clientEnabledStaticMethods = [
    'testStatic',
    'testStaticIterable',
    'testStaticIterableAbort',
    'throwErrorStatic',
    'throwErrorStaticIterable',
  ];
  protected $protectedTags = ['employee'];
  protected $allowlistTags? = ['boss', 'bigcheese'];
  protected $allowlistData? = [
    'name',
    'id',
    'title',
    'department',
    'subordinates',
    'salary',
    'current',
    'startDate',
    'endDate',
    'phone',
    'manager',
    'building',
  ];

  constructor() {
    super();

    this.$addTag('employee');
    this.$data.current = true;
    this.$data.startDate = Date.now();
    this.$data.subordinates = [];
    if (!IS_MANAGER) {
      this.$privateData.push('salary');
    }
  }

  public async $save() {
    // Validate employee data.
    const error = new EntityInvalidDataError('Invalid entity data.');
    if (this.$data.name == null || this.$data.name === '') {
      error.addField('name');
    }
    if (this.$data.title == null || this.$data.title === '') {
      error.addField('title');
    }
    if (this.$data.startDate == null) {
      error.addField('startDate');
    }
    if (error.getFields().length) {
      throw error;
    }
    // Generate employee ID.
    if (this.$data.id == null) {
      this.$data.id = (await this.$nymph.newUID('employee')) ?? undefined;
    }
    return await super.$save();
  }

  public $testMethodStateless(value: number) {
    this.$data.name = 'bad name';
    return value + 1;
  }

  public $testMethod(value: number) {
    this.$data.current = false;
    return value + 2;
  }

  public static testStatic(value: number) {
    return value * 2;
  }

  public static *testStaticIterable(value: number) {
    yield value + 1;
    yield value + 2;
    yield value + 3;
  }

  public static *testStaticIterableAbort(): Iterator<number, void, boolean> {
    let aborted = yield 1;

    if (!aborted) {
      throw new Error(
        "testStaticIterableAbort wasn't aborted after the first iteration.",
      );
    }
  }

  public static throwErrorStatic() {
    throw new BadFunctionCallError('This function only throws errors.');
  }

  public static *throwErrorStaticIterable() {
    yield 1;
    throw new BadFunctionCallError(
      'This function throws errors after the first iteration.',
    );
  }

  public $throwError() {
    throw new BadFunctionCallError('This function only throws errors.');
  }

  public $throwHttpError() {
    throw new HttpError('A 501 HTTP error.', 501);
  }

  public $throwHttpErrorWithDescription() {
    throw new HttpError('A 512 HTTP error.', 512, 'Some Error');
  }

  public static inaccessibleMethod() {
    return true;
  }
}

export class BadFunctionCallError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'BadFunctionCallError';
  }
}

export type EmployeeData = EmployeeBaseData<Employee>;

export class Employee extends Entity<EmployeeData> {
  // The name of the server class
  public static class = 'Employee';

  constructor() {
    super();

    this.$addTag('employee');
    this.$data.current = true;
    this.$data.subordinates = [];
  }

  $testMethod(value: number) {
    return this.$serverCall('$testMethod', [value]);
  }

  $testMethodStateless(value: number) {
    return this.$serverCall('$testMethodStateless', [value], true);
  }

  $throwError() {
    return this.$serverCall('$throwError', []);
  }

  $throwHttpError() {
    return this.$serverCall('$throwHttpError', []);
  }

  $throwHttpErrorWithDescription() {
    return this.$serverCall('$throwHttpErrorWithDescription', []);
  }

  static testStatic(value: number) {
    return this.serverCallStatic('testStatic', [value]);
  }

  static testStaticIterable(value: number) {
    return this.serverCallStaticIterator('testStaticIterable', [value]);
  }

  static testStaticIterableAbort() {
    return this.serverCallStaticIterator('testStaticIterableAbort', []);
  }

  static throwErrorStatic() {
    return this.serverCallStatic('throwErrorStatic', []);
  }

  static throwErrorStaticIterable() {
    return this.serverCallStaticIterator('throwErrorStaticIterable', []);
  }

  static inaccessibleMethod() {
    return this.serverCallStatic('inaccessibleMethod', []);
  }
}

export type RestrictedModelData = {
  name: string;
};

/**
 * This class is a test class that extends the Entity class.
 */
export class RestrictedModel extends EntityServer<RestrictedModelData> {
  static ETYPE = 'restricted';
  static class = 'Restricted';

  public static restEnabled = false;

  constructor() {
    super();

    this.$data.name = '';
  }

  public async $save() {
    // Validate entity data.
    const error = new EntityInvalidDataError('Invalid entity data.');
    if (this.$data.name == null || this.$data.name === '') {
      error.addField('name');
    }
    if (error.getFields().length) {
      throw error;
    }
    return await super.$save();
  }

  $testMethod(value: string) {
    return value;
  }

  public static testStatic(value: number) {
    return value;
  }
}

export type RestrictedData = {
  name: string;
};

export class Restricted extends Entity<RestrictedData> {
  // The name of the server class
  public static class = 'Restricted';

  constructor() {
    super();

    this.$data.name = '';
  }

  $testMethod(value: number) {
    return this.$serverCall('$testMethod', [value]);
  }

  static testStatic(value: number) {
    return this.serverCallStatic('testStatic', [value]);
  }
}

export type PubSubDisabledModelData = {
  name: string;
};

/**
 * This class is a test class that extends the Entity class.
 */
export class PubSubDisabledModel extends EntityServer<PubSubDisabledModelData> {
  static ETYPE = 'pubsub_disabled';
  static class = 'PubSubDisabled';

  public static pubSubEnabled = false;

  constructor() {
    super();

    this.$data.name = '';
  }

  public async $save() {
    // Validate entity data.
    const error = new EntityInvalidDataError('Invalid entity data.');
    if (this.$data.name == null || this.$data.name === '') {
      error.addField('name');
    }
    if (error.getFields().length) {
      throw error;
    }
    return await super.$save();
  }
}

export type PubSubDisabledData = {
  name: string;
};

export class PubSubDisabled extends Entity<PubSubDisabledData> {
  // The name of the server class
  public static class = 'PubSubDisabled';

  constructor() {
    super();

    this.$data.name = '';
  }
}
