import { isBoolean, isNotEmpty, isTrue } from './Is';
import { asString, Text } from './Text';
import { toName } from './Constructor';
import { ctx } from './Context';
import { List, toList } from './List';
import { meta } from './Meta';
import { tryTo } from './Try';
import { Optional } from './Types';
import { OneOrMore, toArray } from './Array';
import { entries } from './Object';

export type Segment = Text & { key?: string; segment?: string; query?: (value: unknown) => string };

export const toSegment = (
  key?: Text,
  {
    segment,
    query,
  }: {
    segment?: string;
    query?: (value: unknown) => string;
  } = {}
): Segment => ({
  key: key as string,
  segment,
  query,
  toString: () => asString(key),
});

export const uri = {
  host: (key?: string): Segment => toSegment(key, { segment: key ?? ctx.env.host ?? '$host' }),
  resource: <Props extends UriExpandProps>(resource: Uri<Props>): Segment => toSegment(toName(resource, 'Uri'), { segment: toName(resource, 'Uri') }),
  segment: (key?: Text): Segment => toSegment(key, { segment: key as string }),
  path: (key: Text): Segment => toSegment(key, { segment: `:${key}` }),
  query: (key: Text): Segment =>
    toSegment(key, {
      query: (value: unknown): string =>
        tryTo(value)
          .is.defined()
          .map(v => encodeURIComponent(asString(v)))
          .map(v => `${key}=${v}`)
          .orElse('') ?? '',
    }),
  boolean: (key: Text): Segment => toSegment(key, { query: (value: unknown): string => (isTrue(value) ? `${key}` : '') }),
};

type Prop = { segment: Segment; value: any };

const toRoute = (...segments: Segment[]): string =>
  toList(segments)
    .mapDefined(s => s.segment)
    .join('/');

export type Uri<Props extends UriExpandProps = UriExpandProps> = {
  id: (id?: unknown) => Uri<Props>;
  ids: (ids: OneOrMore<unknown>) => Uri<Props>;
  query: (q?: unknown) => Uri<Props>;
  sort: (q?: any) => Uri<Props>;
  skip: (n?: number) => Uri<Props>;
  take: (n?: number) => Uri<Props>;
  path: string;
  route: (resource: string) => string;
  isInternal: boolean;
  toString: () => string;
  expand: (props: Partial<Props>) => Uri<Props>;
};

export type UriExpandProps = { q?: string; s?: string; skip?: number; take?: number };

export class EasyUri<Props extends UriExpandProps = UriExpandProps> implements Uri<Props> {
  static readonly id = uri.path('id');
  static readonly ids = uri.query('ids');
  static readonly query = uri.query('q');
  static readonly sort = uri.query('s');
  static readonly skip = uri.query('skip');
  static readonly take = uri.query('take');

  readonly host = uri.host();
  protected resource: Segment = uri.resource(this);

  protected state: any = {};

  constructor(readonly segments: Segment[] = []) {}

  get path(): string {
    return toRoute(uri.segment(''), this.resource, ...this.segments);
  }

  get complete(): string {
    return toRoute(this.host, this.resource, ...this.segments);
  }

  get isInternal(): boolean {
    return toRoute(this.host) === (ctx.env.host ?? '$host');
  }

  protected get props(): List<Prop> {
    return meta(this.state).values<Prop>();
  }

  route = (resource: Optional<string> = this.resource.key): string => toRoute(uri.segment(''), uri.segment(resource?.toLowerCase()), ...this.segments);

  set = (segment: Segment, value?: unknown): this => {
    tryTo(value)
      .is.defined()
      .accept(value => (this.state[segment.key ?? ''] = { segment, value }));
    return this;
  };

  toString(): string {
    return tryTo(() => this.props)
      .map(ps => ps.filter(p => p.segment?.segment))
      .map(ps => ps.reduce((r: string, p: Prop) => r.replace(asString(p.segment.segment), asString(p.value)), this.complete))
      .map(route => ({
        route,
        query: this.props.mapDefined(p => (p.segment?.query ? p.segment?.query(p.value) : undefined))?.join('&'),
      }))
      .map(({ route, query }) => (isNotEmpty(query) ? `${route}?${query}` : route)).value;
  }

  id = (id?: unknown): this => this.set(EasyUri.id, id);
  ids = (ids: OneOrMore<unknown>): this => this.set(EasyUri.ids, toArray(ids).join(','));
  query = (q?: unknown): this => this.set(EasyUri.query, q);
  sort = (s?: any): this => this.set(EasyUri.sort, asString(s));

  skip = (index?: number): this => this.set(EasyUri.skip, index);
  take = (items?: number): this => this.set(EasyUri.take, items);

  expand(props: Partial<Props>): this {
    return entries(props as any)
      .filter(([_, v]) => isNotEmpty(v))
      .reduce((u, [k, v]) => (isBoolean(v) ? u.set(uri.boolean(k), v) : u.set(uri.query(k), toArray(v).join(','))), this);
  }
}

export const clipUri = (uri?: EasyUri | string | null): string =>
  tryTo(() => asString(uri))
    .map(uri => uri.replace(/^(https?:\/\/)?(www\.)?/, ''))
    .map(uri => uri.replace(/\/$/, '')).value;
