import chromeLocation from 'chrome-location'
import * as Path from 'path'
import * as fs from 'fs/promises'
import * as os from 'os'
import {spawn} from 'child_process'
import {pid2title, URL, makeWebDriver, Cookies} from '../helpers'
import {until, WebDriver} from 'selenium-webdriver'

const delayAsync = (ms: number) => new Promise((res) => setTimeout(res, ms))
const isRunning = (pid: number): boolean => {
    try {
        // @see https://nodejs.org/api/process.html#process_process_kill_pid_signal
        return process.kill(pid, 0)
    } catch (error) {
        return error.code === 'EPERM'
    }
}

interface ITempDirectory {
    path: string
    remove: () => Promise<void>
}

const createTmpDir = async (prefix: string): Promise<ITempDirectory> => {
    const path = await fs.mkdtemp(Path.join(os.tmpdir(), prefix))
    const remove = () => fs.rm(path, {recursive: true, force: true, maxRetries: 3})
    return {path, remove}
}

// "Chrome didn't shut down correctly" dialog is caused by their
// crash handler detecting the process was killed. This might be confusing
// to the end user, so we remove it by editing profile preferences
const chromePreventCrashDialog = async (profilePath: string): Promise<void> => {
    const preferencesPath = Path.join(profilePath, 'Default', 'Preferences')
    const jsonBuf = await fs.readFile(preferencesPath)
    const json = JSON.parse(jsonBuf.toString('utf-8'))
    json.profile.exit_type = 'Normal'
    await fs.writeFile(preferencesPath, JSON.stringify(json))
}

const hasNotLoggedIn = async (pid: number): Promise<boolean> => {
    // The final destination title after a successful login
    // includes '- YouTube Studio' regardless of language
    return pid2title(pid)
        .then((title) => !title.includes('- YouTube Studio'))
        .catch((e) => {
            console.error(e)
            return true
        })
}

const runUncontrolledChrome = async (userDataDir: string): Promise<void> => {
    // Spawn a chrome WITHOUT automation features like --remote-debugging-port or --enable automation because they disable google login in most cases
    // This uncontrolled chrome instance saves cookies to the tempDir. Login status is tracked on windows by process title.
    const chromeProcess = spawn(chromeLocation, [
        'https://studio.youtube.com',
        '--no-first-run',
        '--no-default-browser-check',
        '--disable-translate',
        '--disable-default-apps',
        '--disable-popup-blocking',
        '--disable-zero-browsers-open-for-tests',
        `--user-data-dir=${userDataDir}`,
        // possibly confusing "save password" prompt is not possible to
        // hide because it's only possible by "--enable-automation"
    ])

    const pid = chromeProcess.pid
    do {
        await delayAsync(1000)
    } while (isRunning(pid) && (await hasNotLoggedIn(pid)))
    await delayAsync(1000*30);//should not been killed immediately
    chromeProcess.kill()
    await delayAsync(1000)
    await chromePreventCrashDialog(userDataDir)
}

const makeLoggedInChromeProfile = async (): Promise<ITempDirectory> => {
    const modulePrefix = 'node-apiless-youtube-upload-'
    const tempDir = await createTmpDir(modulePrefix)

    // Adding a removal exit hook for tempDir is a bad idea, because it cant be
    // done synchronously for EBUSY reasons (and no async hooks I tried did not
    // work). Therefore we do a cleanup of previous runs rather than trying to
    // clean up the current one on exit
    const {base: tmpBase, dir} = Path.parse(tempDir.path)
    for (const file of await fs.readdir(dir)) {
        if (file === tmpBase || !file.startsWith(modulePrefix)) continue

        const prevProfilePath = Path.join(dir, file)
        console.log('Removing temp profile from previous run', prevProfilePath)
        await fs.rm(prevProfilePath, {recursive: true, force: true, maxRetries: 3})
    }

    return runUncontrolledChrome(tempDir.path)
        .then(() => tempDir)
        .catch((err) => tempDir.remove().then(() => Promise.reject(err)))
}

const fetchCookies = async (driver: WebDriver): Promise<Cookies> => {
    // go to google.com to trigger the saved profile to load faster
    await driver.get(URL.LOADER)
    await driver.sleep(4000)
    await driver.get(URL.GOOGLE)
    // Open youtube studio to test if the login is valid
    await driver.get(URL.YOUTUBE_STUDIO)

    // If cookies are valid, user is now either in Youtube Studio at studio.youtube.com,
    // or User select page at youtube.com
    const currentUrl = await driver.getCurrentUrl()
    const isLoggedIn = [URL.YOUTUBE, URL.YOUTUBE_STUDIO].some((link) => currentUrl.startsWith(link))
    if (!isLoggedIn) {
        throw new Error(
            'The login session could not be loaded (either user never logged in, ' +
                "random lag or google account doesn't have a youtube attached)",
        )
    }

    // Select youtube account (one google account might have many brand accounts)
    await driver.get(URL.SELECT_ACCOUNT_YOUTUBE)
    // Wait until url matches exactly URL.YOUTUBE. Note that account selection url includes URL.YOUTUBE, which we don't want to match.
    await driver.wait(until.urlIs(URL.YOUTUBE), 60 * 1000)

    const webDriverCookies = await driver.manage().getCookies()

    return new Cookies(webDriverCookies)
}

export default async (): Promise<Cookies> => {
    return new Promise((resolve, reject) =>{
        (async ()=>{
            let profilePath: ITempDirectory
            let webDriver: WebDriver
            profilePath = await makeLoggedInChromeProfile()
            webDriver = await makeWebDriver({automation: true, userDataDir: profilePath.path})
            fetchCookies(webDriver).then(function(cookies){
                resolve(cookies);
            }).catch(function(e){
                reject(e);
            }).finally(async () => {
                if (webDriver) await webDriver.quit()
                if (profilePath) await profilePath.remove() 
            });
        })()
    });
}
