import type { Object } from 'ts-toolbelt';
import type Koa from 'koa';
import type { Store as ReduxStore } from 'redux';
import type * as ActionCreators from './core/action-creators';
import type { ActionErrorType, UpdateErrorType } from './core/errors';
import type { Flow } from './core/flow';
import type { CreateGameReducer } from './core/reducer';
import type { INVALID_MOVE } from './core/constants';
import type { GameMethod } from './core/game-methods';
import type { Auth } from './server/auth';
import type * as StorageAPI from './server/db/base';
import type { EventsAPI } from './plugins/plugin-events';
import type { LogAPI } from './plugins/plugin-log';
import type { RandomAPI } from './plugins/random/random';
import type { Operation } from 'rfc6902';
export type { StorageAPI };

export type AnyFn = (...args: any[]) => any;

// "Public" state to be communicated to clients.
export interface State<G extends any = any> {
  G: G;
  ctx: Ctx;
  deltalog?: Array<LogEntry>;
  plugins: {
    [pluginName: string]: PluginState;
  };
  _undo: Array<Undo<G>>;
  _redo: Array<Undo<G>>;
  _stateID: number;
}

export type ErrorType = UpdateErrorType | ActionErrorType;

export interface ActionError {
  type: ErrorType;
  // TODO(#723): Figure out if we want to strongly type payloads.
  payload?: any;
}

export interface TransientMetadata {
  error?: ActionError;
}

// TODO(#732): Actually define a schema for the action dispatch results.
export type ActionResult = any;

// "Private" state that may include garbage that should be stripped before
// being handed back to a client.
export interface TransientState<G extends any = any> extends State<G> {
  transients?: TransientMetadata;
}

export type PartialGameState = Pick<State, 'G' | 'ctx' | 'plugins'>;

export type StageName = string;
export type PlayerID = string;

export type StageArg =
  | StageName
  | {
      stage?: StageName;
      /** @deprecated Use `minMoves` and `maxMoves` instead. */
      moveLimit?: number;
      minMoves?: number;
      maxMoves?: number;
    };

export type ActivePlayersArg =
  | PlayerID[]
  | {
      currentPlayer?: StageArg;
      others?: StageArg;
      all?: StageArg;
      value?: Record<PlayerID, StageArg>;
      minMoves?: number;
      maxMoves?: number;
      /** @deprecated Use `minMoves` and `maxMoves` instead. */
      moveLimit?: number;
      revert?: boolean;
      next?: ActivePlayersArg;
    };

export interface ActivePlayers {
  [playerID: string]: StageName;
}

export interface Ctx {
  numPlayers: number;
  playOrder: Array<PlayerID>;
  playOrderPos: number;
  activePlayers: null | ActivePlayers;
  currentPlayer: PlayerID;
  numMoves?: number;
  gameover?: any;
  turn: number;
  phase: string;
  _activePlayersMinMoves?: Record<PlayerID, number>;
  _activePlayersMaxMoves?: Record<PlayerID, number>;
  _activePlayersNumMoves?: Record<PlayerID, number>;
  _prevActivePlayers?: Array<{
    activePlayers: null | ActivePlayers;
    _activePlayersMinMoves?: Record<PlayerID, number>;
    _activePlayersMaxMoves?: Record<PlayerID, number>;
    _activePlayersNumMoves?: Record<PlayerID, number>;
  }>;
  _nextActivePlayers?: ActivePlayersArg;
  _random?: {
    seed: string | number;
  };
}

export interface DefaultPluginAPIs {
  events: EventsAPI;
  log: LogAPI;
  random: RandomAPI;
}

export interface PluginState {
  data: any;
  api?: any;
}

export interface LogEntry {
  action:
    | ActionShape.MakeMove
    | ActionShape.GameEvent
    | ActionShape.Undo
    | ActionShape.Redo;
  _stateID: number;
  turn: number;
  phase: string;
  redact?: boolean;
  automatic?: boolean;
  metadata?: any;
  patch?: Operation[];
}

export interface PluginContext<
  API extends any = any,
  Data extends any = any,
  G extends any = any
> {
  G: G;
  ctx: Ctx;
  game: Game;
  api: API;
  data: Data;
}

export interface Plugin<
  API extends any = any,
  Data extends any = any,
  G extends any = any
> {
  name: string;
  noClient?: (context: PluginContext<API, Data, G>) => boolean;
  setup?: (setupCtx: { G: G; ctx: Ctx; game: Game<G> }) => Data;
  isInvalid?: (
    context: Omit<PluginContext<API, Data, G>, 'api'>
  ) => false | string;
  action?: (data: Data, payload: ActionShape.Plugin['payload']) => Data;
  api?: (context: {
    G: G;
    ctx: Ctx;
    game: Game<G>;
    data: Data;
    playerID?: PlayerID;
  }) => API;
  flush?: (context: PluginContext<API, Data, G>) => Data;
  dangerouslyFlushRawState?: (flushCtx: {
    state: State<G>;
    game: Game<G>;
    api: API;
    data: Data;
  }) => State<G>;
  fnWrap?: (
    moveOrHook: (context: FnContext<G>, ...args: any[]) => any,
    methodType: GameMethod
  ) => (context: FnContext<G>, ...args: any[]) => any;
  playerView?: (context: {
    G: G;
    ctx: Ctx;
    game: Game<G>;
    data: Data;
    playerID?: PlayerID | null;
  }) => any;
}

export type FnContext<
  G extends any = any,
  PluginAPIs extends Record<string, unknown> = Record<string, unknown>
> = PluginAPIs &
  DefaultPluginAPIs & {
    G: G;
    ctx: Ctx;
  };

export type MoveFn<
  G extends any = any,
  PluginAPIs extends Record<string, unknown> = Record<string, unknown>
> = (
  context: FnContext<G, PluginAPIs> & { playerID: PlayerID },
  ...args: any[]
) => void | G | typeof INVALID_MOVE;

export interface LongFormMove<
  G extends any = any,
  PluginAPIs extends Record<string, unknown> = Record<string, unknown>
> {
  move: MoveFn<G, PluginAPIs>;
  redact?: boolean | ((context: { G: G; ctx: Ctx }) => boolean);
  noLimit?: boolean;
  client?: boolean;
  undoable?: boolean | ((context: { G: G; ctx: Ctx }) => boolean);
  ignoreStaleStateID?: boolean;
}

export type Move<
  G extends any = any,
  PluginAPIs extends Record<string, unknown> = Record<string, unknown>
> = MoveFn<G, PluginAPIs> | LongFormMove<G, PluginAPIs>;

export interface MoveMap<
  G extends any = any,
  PluginAPIs extends Record<string, unknown> = Record<string, unknown>
> {
  [moveName: string]: Move<G, PluginAPIs>;
}

export interface PhaseConfig<
  G extends any = any,
  PluginAPIs extends Record<string, unknown> = Record<string, unknown>
> {
  start?: boolean;
  next?: ((context: FnContext<G, PluginAPIs>) => string | void) | string;
  onBegin?: (context: FnContext<G, PluginAPIs>) => void | G;
  onEnd?: (context: FnContext<G, PluginAPIs>) => void | G;
  endIf?: (
    context: FnContext<G, PluginAPIs>
  ) => boolean | void | { next: string };
  moves?: MoveMap<G, PluginAPIs>;
  turn?: TurnConfig<G, PluginAPIs>;
  wrapped?: {
    endIf?: (state: State<G>) => boolean | void | { next: string };
    onBegin?: (state: State<G>) => void | G;
    onEnd?: (state: State<G>) => void | G;
    next?: (state: State<G>) => string | void;
  };
}

export interface StageConfig<
  G extends any = any,
  PluginAPIs extends Record<string, unknown> = Record<string, unknown>
> {
  moves?: MoveMap<G, PluginAPIs>;
  next?: string;
}

export interface StageMap<
  G extends any = any,
  PluginAPIs extends Record<string, unknown> = Record<string, unknown>
> {
  [stageName: string]: StageConfig<G, PluginAPIs>;
}

export interface TurnOrderConfig<
  G extends any = any,
  PluginAPIs extends Record<string, unknown> = Record<string, unknown>
> {
  first: (context: FnContext<G, PluginAPIs>) => number;
  next: (context: FnContext<G, PluginAPIs>) => number | undefined;
  playOrder?: (context: FnContext<G, PluginAPIs>) => PlayerID[];
}

export interface TurnConfig<
  G extends any = any,
  PluginAPIs extends Record<string, unknown> = Record<string, unknown>
> {
  activePlayers?: ActivePlayersArg;
  minMoves?: number;
  maxMoves?: number;
  /** @deprecated Use `minMoves` and `maxMoves` instead. */
  moveLimit?: number;
  onBegin?: (context: FnContext<G, PluginAPIs>) => void | G;
  onEnd?: (context: FnContext<G, PluginAPIs>) => void | G;
  endIf?: (
    context: FnContext<G, PluginAPIs>
  ) => boolean | void | { next: PlayerID };
  onMove?: (
    context: FnContext<G, PluginAPIs> & { playerID: PlayerID }
  ) => void | G;
  stages?: StageMap<G, PluginAPIs>;
  order?: TurnOrderConfig<G, PluginAPIs>;
  wrapped?: {
    endIf?: (state: State<G>) => boolean | void | { next: PlayerID };
    onBegin?: (state: State<G>) => void | G;
    onEnd?: (state: State<G>) => void | G;
    onMove?: (state: State<G> & { playerID: PlayerID }) => void | G;
  };
}

export interface PhaseMap<
  G extends any = any,
  PluginAPIs extends Record<string, unknown> = Record<string, unknown>
> {
  [phaseName: string]: PhaseConfig<G, PluginAPIs>;
}

export type AiEnumerate = Array<
  | { event: string; args?: any[] }
  | { move: string; args?: any[] }
  | ActionShape.MakeMove
  | ActionShape.GameEvent
>;

export interface Game<
  G extends any = any,
  PluginAPIs extends Record<string, unknown> = Record<string, unknown>,
  SetupData extends any = any
> {
  name?: string;
  minPlayers?: number;
  maxPlayers?: number;
  deltaState?: boolean;
  disableUndo?: boolean;
  seed?: string | number;
  setup?: (
    context: PluginAPIs & DefaultPluginAPIs & { ctx: Ctx },
    setupData?: SetupData
  ) => G;
  validateSetupData?: (
    setupData: SetupData | undefined,
    numPlayers: number
  ) => string | undefined;
  moves?: MoveMap<G, PluginAPIs>;
  phases?: PhaseMap<G, PluginAPIs>;
  turn?: TurnConfig<G, PluginAPIs>;
  events?: {
    endGame?: boolean;
    endPhase?: boolean;
    endTurn?: boolean;
    setPhase?: boolean;
    endStage?: boolean;
    setStage?: boolean;
    pass?: boolean;
    setActivePlayers?: boolean;
  };
  endIf?: (context: FnContext<G, PluginAPIs>) => any;
  onEnd?: (context: FnContext<G, PluginAPIs>) => void | G;
  playerView?: (context: { G: G; ctx: Ctx; playerID: PlayerID | null }) => any;
  plugins?: Array<Plugin<any, any, G>>;
  ai?: {
    enumerate: (G: G, ctx: Ctx, playerID: PlayerID) => AiEnumerate;
  };
  processMove?: (
    state: State<G>,
    action: ActionPayload.MakeMove
  ) => State<G> | typeof INVALID_MOVE;
  flow?: ReturnType<typeof Flow>;
}

export type Undo<G extends any = any> = {
  G: G;
  ctx: Ctx;
  plugins: {
    [pluginName: string]: PluginState;
  };
  moveType?: string;
  playerID?: PlayerID;
};

export namespace Server {
  export type GenerateCredentials = (
    ctx: Koa.DefaultContext
  ) => Promise<string> | string;

  export type AuthenticateCredentials = (
    credentials: string,
    playerMetadata: PlayerMetadata
  ) => Promise<boolean> | boolean;

  export type PlayerMetadata = {
    id: number;
    name?: string;
    credentials?: string;
    data?: any;
    isConnected?: boolean;
  };

  export interface MatchData {
    gameName: string;
    players: { [id: number]: PlayerMetadata };
    setupData?: any;
    gameover?: any;
    nextMatchID?: string;
    unlisted?: boolean;
    createdAt: number;
    updatedAt: number;
  }

  export type AppCtx = Koa.DefaultContext & {
    db: StorageAPI.Async | StorageAPI.Sync;
    auth: Auth;
  };

  export type App = Koa<Koa.DefaultState, AppCtx>;
}

export namespace LobbyAPI {
  export type GameList = string[];
  type PublicPlayerMetadata = Omit<Server.PlayerMetadata, 'credentials'>;
  export type Match = Omit<Server.MatchData, 'players'> & {
    matchID: string;
    players: PublicPlayerMetadata[];
  };
  export interface MatchList {
    matches: Match[];
  }
  export interface CreatedMatch {
    matchID: string;
  }
  export interface JoinedMatch {
    playerID: string;
    playerCredentials: string;
  }
  export interface NextMatch {
    nextMatchID: string;
  }
}

export type Reducer = ReturnType<typeof CreateGameReducer>;
export type Store = ReduxStore<State, ActionShape.Any>;

export namespace CredentialedActionShape {
  export type MakeMove = ReturnType<typeof ActionCreators.makeMove>;
  export type GameEvent = ReturnType<typeof ActionCreators.gameEvent>;
  export type Plugin = ReturnType<typeof ActionCreators.plugin>;
  export type AutomaticGameEvent = ReturnType<
    typeof ActionCreators.automaticGameEvent
  >;
  export type Undo = ReturnType<typeof ActionCreators.undo>;
  export type Redo = ReturnType<typeof ActionCreators.redo>;
  export type Any =
    | MakeMove
    | GameEvent
    | AutomaticGameEvent
    | Undo
    | Redo
    | Plugin;
}

export namespace ActionShape {
  type StripCredentials<T extends CredentialedActionShape.Any> = Object.P.Omit<
    T,
    ['payload', 'credentials']
  >;
  export type MakeMove = StripCredentials<CredentialedActionShape.MakeMove>;
  export type GameEvent = StripCredentials<CredentialedActionShape.GameEvent>;
  export type Plugin = StripCredentials<CredentialedActionShape.Plugin>;
  export type AutomaticGameEvent =
    StripCredentials<CredentialedActionShape.AutomaticGameEvent>;
  export type Sync = ReturnType<typeof ActionCreators.sync>;
  export type Update = ReturnType<typeof ActionCreators.update>;
  export type Patch = ReturnType<typeof ActionCreators.patch>;
  export type Reset = ReturnType<typeof ActionCreators.reset>;
  export type Undo = StripCredentials<CredentialedActionShape.Undo>;
  export type Redo = StripCredentials<CredentialedActionShape.Redo>;
  // Private type used only for internal error processing.
  // Included here to preserve type-checking of reducer inputs.
  export type StripTransients = ReturnType<
    typeof ActionCreators.stripTransients
  >;
  export type Any =
    | MakeMove
    | GameEvent
    | AutomaticGameEvent
    | Sync
    | Update
    | Patch
    | Reset
    | Undo
    | Redo
    | Plugin
    | StripTransients;
}

export namespace ActionPayload {
  type GetPayload<T extends ActionShape.Any> = Object.At<T, 'payload'>;
  export type MakeMove = GetPayload<ActionShape.MakeMove>;
  export type GameEvent = GetPayload<ActionShape.GameEvent>;
}

export type FilteredMetadata = {
  id: number;
  name?: string;
  isConnected?: boolean;
}[];

export interface SyncInfo {
  state: State;
  filteredMetadata: FilteredMetadata;
  initialState: State;
  log: LogEntry[];
}

export interface ChatMessage {
  id: string;
  sender: PlayerID;
  payload: any;
}
