import { WebContents } from 'electron';
import os from 'os';
import path from 'path';
import { Readable } from 'stream';
import { URL } from 'url';
import { createReadStream, remove } from 'fs-extra';
import { injectable } from 'inversify';
import { inject } from '../../core/inject';
import { UrlAppWindow } from '../app-window/url-app-window';
import { WindowClosedError } from '../app-window/window-closed-error';
import { ISessionHelper } from '../session/i-session-helper';
import { INhentaiAppWindow } from './i-nhentai-app-window';
import { IFavorite } from './nhentai';
import {
  url as nhentaiUrl,
  hostname as nhentaiHostname,
  paths as nhentaiPaths,
  getFavoritePageUrl,
  nextFavoritePageSelector,
  coverLinkSelector,
  downloadLinkId,
  getGalleryId,
} from './nhentai-util';

const waitInterval = 2000;

@injectable()
export class NhentaiAppWindow extends UrlAppWindow implements INhentaiAppWindow {
  public constructor(@inject('session-helper') sessionHelper: ISessionHelper) {
    super(sessionHelper, nhentaiUrl);
  }

  /**
   * @throws WindowClosedError when this._window is null
   */
  public async getFavorites(): Promise<NodeJS.ReadableStream> {
    const error = new WindowClosedError();

    if (this.isClosed()) {
      await this.open();
    }

    if (!(await this.isLoggedIn())) {
      await this.login();
    }

    if (!this._window) {
      throw error;
    }

    const bookUrls: string[] = [];
    for await (const wc of this.getFavoritePageWebContentsGenerator()) {
      bookUrls.push(
        ...((await wc.executeJavaScript(
          `Array.from(document.querySelectorAll('${coverLinkSelector}')).map((el) => el.href)`
        )) as string[])
      );
    }

    const readable = Readable.from(
      (async function* (thisArg): AsyncGenerator<IFavorite> {
        for (const bookUrl of bookUrls) {
          yield await thisArg.getBookTorrent(bookUrl);
        }
      })(this),
      {
        objectMode: true,
      }
    );
    readable.once('close', this.close);

    return readable;
  }

  protected getCsp(): IContentSecurityPolicy {
    return {
      'default-src': ['nhentai.net'],
      'script-src': ['nhentai.net', "'unsafe-eval'"],
      'script-src-elem': ['*.nhentai.net', "'unsafe-inline'", '*.google.com', '*.gstatic.com'],
      'style-src': [
        '*.nhentai.net',
        "'unsafe-inline'",
        'fonts.googleapis.com',
        'cdnjs.cloudflare.com',
        'maxcdn.bootstrapcdn.com',
        '*.gstatic.com',
      ],
      'img-src': ['*.nhentai.net', 'data:', '*.gstatic.com', '*.google.com'],
      'font-src': ['fonts.gstatic.com', 'cdnjs.cloudflare.com', 'maxcdn.bootstrapcdn.com'],
      'frame-src': ['*.google.com'],
      'connect-src': ['nhentai.net', '*.google.com'],
      'worker-src': ['*.google.com'],
    };
  }

  protected onWillNavigate(event: Electron.Event, navigationUrl: string): void {
    const url = new URL(navigationUrl);

    if (url.hostname !== nhentaiHostname) {
      event.preventDefault();
    }
  }

  /**
   * @throws WindowClosedError when this._window is null
   */
  private async isLoggedIn(): Promise<boolean> {
    if (!this._window) {
      throw new WindowClosedError();
    }
    return this._window.webContents
      .executeJavaScript(
        `fetch('${
          nhentaiUrl + nhentaiPaths.favorites
        }', {credentials: 'include', redirect: 'manual'}).then((res) => res.status)`
      )
      .then((status: number) => status === HttpCode.OK);
  }

  /**
   * @throws WindowClosedError when this._window is null
   */
  private async login(): Promise<void> {
    if (!this._window) {
      throw new WindowClosedError();
    }
    await this.loadUrlSafe(nhentaiUrl + nhentaiPaths.login);

    return new Promise((resolve, reject) => {
      const timeout = setInterval(() => {
        this.isLoggedIn()
          .then((loggedIn) => {
            if (loggedIn) {
              clearTimeout(timeout);
              resolve();
            }
          })
          .catch((reason) => reject(reason));
      }, waitInterval);
    });
  }

  private async *getFavoritePageWebContentsGenerator(): AsyncGenerator<WebContents, undefined> {
    const error = new WindowClosedError();
    if (!this._window) {
      throw error;
    }
    await this.loadUrlSafe(getFavoritePageUrl(1));
    yield this._window.webContents;
    do {
      try {
        const hasNextPage = (await this._window.webContents.executeJavaScript(
          `!!document.querySelector('${nextFavoritePageSelector}')`
        )) as boolean;
        if (hasNextPage) {
          yield new Promise<WebContents>((resolve) => {
            if (this._window) {
              this._window.webContents.once('did-finish-load', () => {
                if (this._window) {
                  resolve(this._window.webContents);
                } else {
                  throw error;
                }
              });
              void this._window.webContents.executeJavaScript(
                `document.querySelector('${nextFavoritePageSelector}').click()`
              );
            } else {
              throw error;
            }
          });
        } else {
          break;
        }
      } catch {
        break;
      }
    } while (true);
    return undefined;
  }

  private async getBookTorrent(bookUrl: string): Promise<IFavorite> {
    const galleryId = getGalleryId(bookUrl);
    const fileName = `${galleryId}.torrent`;
    const filePath = path.resolve(os.tmpdir(), fileName);
    await this.loadUrlSafe(bookUrl);
    await new Promise<string>((resolve, reject) => {
      if (!this._window) {
        throw new WindowClosedError();
      }
      this._window.webContents.session.once('will-download', (event, item) => {
        item.setSavePath(filePath);
        item.once('done', (doneEvent, state) => {
          switch (state) {
            case 'completed':
              resolve();
              break;
            case 'cancelled':
            case 'interrupted':
            default:
              reject(new Error(state));
              break;
          }
        });
        item.on('updated', () => {
          item.resume();
        });
      });
      void this._window.webContents.executeJavaScript(`document.getElementById('${downloadLinkId}').click()`);
    });

    const readable = createReadStream(filePath, { emitClose: true });

    readable.once('close', () => {
      void remove(filePath);
    });

    return {
      name: fileName,
      torrentFile: readable,
    };
  }
}
