import md5 from "md5";

const isAssetUrlOptions = (options: Options): options is AssetUrlOptions => {
  return options && "assetUrl" in options;
};

type AssetUrlOptions = {
  assetUrl: string;
};

type TokenOptions = {
  token: string;
  videoId: string;
};

type Domain = {
  live?: string;
  vod?: string;
  embed?: string;
  collector?: string;
};

type Options = (AssetUrlOptions | TokenOptions) & { domains?: Domain };

type VideoIdPrivateToken = {
  host?: string;
  videoId: string;
  privateToken: string;
  environment?: Environment;
  videoType?: VideoType;
  subdomain?: string;
};

type Environment = "production" | "staging";

type VideoType = "vod" | "live";

type UrlType = {
  re: RegExp;
  type: VideoType;
  env: Environment;
};

export class PrivateVideoSession {
  private videoId: string;
  private privateToken: string;
  private environment: Environment;
  private videoType: VideoType;
  private sessionToken?: string;
  private domains: Domain;

  constructor(options: Options) {
    this.domains = options.domains || {};

    const videoIdPrivateToken: VideoIdPrivateToken = isAssetUrlOptions(options)
      ? this.initFromAssetUrl(options.assetUrl)
      : this.initFromToken(options.token, options.videoId);

    this.videoId = videoIdPrivateToken.videoId;
    this.privateToken = videoIdPrivateToken.privateToken;
    this.environment = videoIdPrivateToken.environment || "production";
    this.videoType = videoIdPrivateToken.videoType || "vod";

    if (Object.keys(this.domains).length > 0 && !this.domains[this.videoType]) {
      throw new Error(
        `The ${this.videoType} domain is required when using ${this.videoType} assets with custom domains.`
      );
    }
  }

  public async getHlsUrl(): Promise<string> {
    return this.videoType === "vod"
      ? `https://${this.getMediaHost()}/vod/${this.videoId}/token/${
          this.privateToken
        }/hls/manifest.m3u8?avh=${await this.getSessionToken()}`
      : `https://${this.getMediaHost()}/private/${this.privateToken}/${
          this.videoId
        }.m3u8?avh=${await this.getSessionToken()}`;
  }

  public async getPlayerUrl(): Promise<string> {
    return `https://${this.getEmbedHost()}/${this.videoType}/${
      this.videoId
    }?token=${this.privateToken}&avh=${await this.getSessionToken()}`;
  }

  public async getMp4Url(): Promise<string> {
    if (this.videoType === "live") {
      throw new Error("The mp4 url is not available for live streams.");
    }
    return `https://${this.getMediaHost()}/vod/${this.videoId}/token/${
      this.privateToken
    }/mp4/source.mp4?avh=${await this.getSessionToken()}`;
  }

  public async getThumbnailUrl(): Promise<string> {
    return this.videoType === "vod"
      ? `https://${this.getMediaHost()}/vod/${this.videoId}/token/${
          this.privateToken
        }/thumbnail.jpg?avh=${await this.getSessionToken()}`
      : `https://${this.getMediaHost()}/private/${this.privateToken}/${
          this.videoId
        }/thumbnail.jpg?avh=${await this.getSessionToken()}`;
  }

  public async getSessionToken(): Promise<string> {
    if (this.sessionToken) {
      return this.sessionToken;
    }

    const sessionStorageKey = md5(`xTokenSession_${this.privateToken}`);

    const storageSessionToken = sessionStorage.getItem(sessionStorageKey);
    if (storageSessionToken) {
      this.sessionToken = storageSessionToken;
      return storageSessionToken;
    }

    const newSessionToken = await this.fetchSessionToken();
    if (newSessionToken) {
      sessionStorage.setItem(sessionStorageKey, newSessionToken);
      this.sessionToken = newSessionToken;
    }
    return newSessionToken;
  }

  private getEmbedHost(): string {
    if (this.domains.embed) {
      return this.domains.embed;
    }
    return this.environment === "production"
      ? "embed.api.video"
      : "embed.staging.api.video";
  }

  private getMediaHost(): string {
    if (this.domains[this.videoType]) {
      return this.domains[this.videoType]!;
    }

    return this.environment === "production"
      ? `${this.videoType}.api.video`
      : `${this.videoType}.staging.api.video`;
  }

  private initFromAssetUrl(privateAssetUrl: string): VideoIdPrivateToken {
    const urls: UrlType[] = [
      {
        re: new RegExp(
          `https:\/\/${
            this.domains.vod || "vod.api.video"
          }\/vod\/(?<videoId>[^\/]*)\/token\/(?<privateToken>[^\/]*)\/.*`,
          "gm"
        ),
        type: "vod",
        env: "production",
      },
      {
        re: new RegExp(
          `https:\/\/${
            this.domains.vod || "vod.staging.api.video"
          }\/vod\/(?<videoId>[^\/]*)\/token\/(?<privateToken>[^\/]*)\/.*`,
          "gm"
        ),
        type: "vod",
        env: "staging",
      },
      {
        re: new RegExp(
          `https:\/\/${
            this.domains.embed || "embed.api.video"
          }\/vod\/(?<videoId>[^\/]*)\\?token=(?<privateToken>[^\/]*)`,
          "gm"
        ),
        type: "vod",
        env: "production",
      },
      {
        re: new RegExp(
          `https:\/\/${
            this.domains.embed || "embed.staging.api.video"
          }\/vod\/(?<videoId>[^\/]*)\\?token=(?<privateToken>[^\/]*)`,
          "gm"
        ),
        type: "vod",
        env: "staging",
      },
      {
        re: new RegExp(
          `https:\/\/${
            this.domains.live || "live.api.video"
          }\/private\/(?<privateToken>[^\/]*)\/(?<videoId>[^\.]*)\..*`,
          "gm"
        ),
        type: "live",
        env: "production",
      },
      {
        re: new RegExp(
          `https:\/\/${
            this.domains.live || "live.staging.api.video"
          }\/private\/(?<privateToken>[^\/]*)\/(?<videoId>[^\.]*)\..*`,
          "gm"
        ),
        type: "live",
        env: "staging",
      },
      {
        re: new RegExp(
          `https:\/\/${
            this.domains.embed || "embed.api.video"
          }\/live\/(?<videoId>[^\/]*)\\?token=(?<privateToken>[^\/]*)`,
          "gm"
        ),
        type: "live",
        env: "production",
      },
      {
        re: new RegExp(
          `https:\/\/${
            this.domains.embed || "embed.staging.api.video"
          }\/live\/(?<videoId>[^\/]*)\\?token=(?<privateToken>[^\/]*)`,
          "gm"
        ),
        type: "live",
        env: "staging",
      },
    ];

    for (const url of urls) {
      const parsed = url.re.exec(privateAssetUrl);

      if (parsed?.groups?.videoId && parsed.groups.privateToken) {
        return {
          videoId: parsed.groups.videoId,
          privateToken: parsed.groups.privateToken,
          environment: url.env,
          videoType: url.type,
        };
      }
    }

    throw new Error(
      "The provided URL doesn't look like an api.video private asset URL."
    );
  }

  private initFromToken(
    privateToken: string,
    videoId: string
  ): VideoIdPrivateToken {
    if (!privateToken || !videoId) {
      throw new Error("The provided token and videoId are invalid.");
    }

    return {
      videoId,
      privateToken,
    };
  }

  private async fetchSessionToken(): Promise<string> {
    const response = await fetch(
      this.videoType === "vod"
        ? `https://${this.getMediaHost()}/vod/${this.videoId}/token/${
            this.privateToken
          }/session`
        : `https://${this.getMediaHost()}/private/${this.privateToken}/${
            this.videoId
          }/session`
    );

    if (response.status !== 200) {
      throw new Error(
        "Can't retrieve a session token. The private token has probably already been used."
      );
    }

    const json = await response.json();
    return json.session_token;
  }
}
