# Copyright (C) 2022  Milosz Piglas
#
# This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

from json import loads
from random import randint
from shutil import which
from subprocess import Popen
from threading import Lock, Thread
from urllib.parse import urlparse, parse_qs
from urllib.request import urlopen

import logging

LOG = logging.getLogger(__name__)
LOG.addHandler(logging.NullHandler())

class AuthException(Exception):

    def __init__(self, msg):
        Exception.__init__(self, msg)

class Process:
    def __init__(
        self, launch_uri, name="chrome", incognito=False, temp_profile=False, port=None
    ):
        self.proc_args = [which(name)]
        if incognito:
            self.proc_args.append("--incognito")
        if temp_profile:
            self.proc_args.append("--temp-profile")
        self.debug_port = port
        if port is None:
            self.debug_port = randint(2000, 4000)
        self.proc_args.append("--remote-debugging-port=" + str(self.debug_port))
        self.proc_args.append(launch_uri)

    def _start(self):
        self._proc = Popen(self.proc_args)
        LOG.debug('Starting browser ' + str(self.proc_args))
        self._proc.communicate()
        if self.lock.locked():
            LOG.warn('Browser closed by user')
            self.lock.release()

    def launch(self):
        self.lock = Lock()
        self.lock.acquire(False)
        self._t = Thread(target=self._start)
        self._t.start()
        return self.lock

    def stop(self):
        LOG.debug('Terminating browser')
        self.lock.release()
        self._proc.terminate()


def _wait_redirect(target, debug_port, lock):
    redirect = None
    while not redirect and lock.locked():
        try:
            uri = "http://127.0.0.1:{}/json".format(debug_port)
            resp = _http_get(uri)
            obj = loads(resp.decode("utf-8"))
            for elem in obj:
                elem_type = elem.get("type", None)
                elem_url = elem.get("url", "")
                if elem_type == "page" and elem_url.startswith(target):
                    redirect = elem_url
        except Exception as e:
            continue
    else:
        return redirect


def _get_access_code(app_id, browser, redirect, port, incognito, temp_profile):
    login_uri = "https://www.facebook.com/v15.0/dialog/oauth?client_id={}&redirect_uri={}&state=get_login".format(
        app_id, redirect
    )

    browser = Process(login_uri, browser, incognito, temp_profile, port)
    lock = browser.launch()
    access = _wait_redirect(redirect, browser.debug_port, lock)
    browser.stop()

    if access:
        access_url = urlparse(access)
        query = parse_qs(access_url.query)
        return query.get("code", [None])[0]
    else:
        raise AuthException("Authentication failed")


def _http_get(uri):
    with urlopen(uri) as sock:
        return sock.read(4096)


def get_standalone(
    app_id,
    app_secret,
    browser="chrome",
    redirect="https://example.com",
    port=None,
    incognito=False,
    temp_profile=False,
):
    """
    Performs authentication and obtains access token to Facebook Graph API

    Function is intended for standalone applications, that integrate with Facebook Graph API.
    It opens web browser with Facebook login form. After successful authentication function
    reads code from redirect url and obtains access token. Depending on settings of Facebook
    aplication, a token can be used for requests to Meta services, that support Graph API.

    Function requires Facebook Application with activated Facebook Login.

    Parameters:
       app_id: Facebook Application Id
       app_secret: Facebook Application secret code
       browser: web browser, that is used for displaying login form
       redirect: URL to which web browser is redirected after successful user authentication
       port: Chrome debug port. If None then random port is selected
       incognito: if True then Chrome starts in incognito mode
       temp_profile: if True then Chrome starts with temporary profile

    Returns:
       JSON object, that represents Facebook Authentication Token
    Throws:
       AuthException if authentication fails from any reason
    """

    code = _get_access_code(app_id, browser, redirect, port, incognito, temp_profile)
    token_uri = "https://graph.facebook.com/v15.0/oauth/access_token?client_id={}&client_secret={}&code={}&redirect_uri={}/".format(
        app_id, app_secret, code, redirect
    )
    return loads(_http_get(token_uri))


if __name__ == "__main__":
    try:
        from getpass import getpass

        app_id = input("App ID: ")
        secret = getpass("App secret: ")
        t = get_standalone(app_id, secret, "chromium")
        print(t)
    except Exception as e:
        print(e.read(4096))
