"""
    Copyright (C) 2021-present, Murdo B. Maclachlan

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU 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 General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>.

    Contact me at murdomaclachlan@duck.com
"""

import praw
import socket
import sys
import webbrowser
from configparser import NoSectionError
from praw.exceptions import MissingRequiredAttributeException
from random import randint
from typing import Dict
from .creds import add_refresh_token, create_credentials, get_credentials
from .globals import Globals
from .logger import Log

global Globals, Log


def check_failure(client: object, params: Dict, state: str) -> None:
    """Checks for an authorisation failure, either due to a state mismatch or Reddit
    throwing an error in the return parameters.

    :param client:  A web client object.
    :param params:  The parameters received by the web client.
    :param state:   The expected state.
    """
    if state != params["state"]:
        send_message(
            client, f'State mismatch. Expected: {state} Received: {params["state"]}'
        )
        Log.new(
            f'State mismatch. Expected: {state} Received: {params["state"]}', "FATAL"
        )
        sys.exit()
    elif "error" in params:
        send_message(client, params["error"])
        Log.new([params["error"]], "FATAL")
        sys.exit()


def init() -> object:
    """Initialises the Reddit instance, creating a new praw.ini if none is found.

    :return: praw.Reddit instance.
    """
    try:
        return login()
    # Catch for invalid praw.ini, will create a new one then restart the program; the
    # restart is required due to current PRAW limitations. :'(
    except (NoSectionError, MissingRequiredAttributeException, KeyError):
        if create_credentials():
            Log.new(
                "praw.ini successfully created, program restart required for this"
                + " to take effect.",
                "INFO"
            )
        else:
            Log.new("Failed to create praw.ini file, something went wrong.", "WARNING")
        sys.exit()


def login() -> object:
    """Handles the Reddit login and authorisation using credentials from praw.ini; will
    also handle initial refresh token setup if 2FA is enabled for the account.

    :return: praw.Reddit instance.
    """
    creds = get_credentials()

    constants = {
        "user_agent": f"{sys.platform}:tcf:v{Globals.VERSION}:by /u/MurdoMaclachlan",
        "client_id": creds["client_id"],
        "client_secret": creds["client_secret"],
        "username": creds["username"],
    }

    # Indicates user will not have authorised TCF yet
    if "refresh_token" not in creds.keys():
        reddit = praw.Reddit(redirect_uri=creds["redirect_uri"], **constants)
        state = str(randint(0, 65000))
        scopes = ["privatemessages", "read", "submit"]
        url = reddit.auth.url(scopes, state, "permanent")
        Log.new(
            "TCF has not yet been authorised with your account. The program"
            + " will now open a tab in your browser to complete this process.",
            "INFO"
        )
        webbrowser.open(url)

        client = receive_connection()
        data = client.recv(1024).decode("utf-8")
        param_tokens = data.split(" ", 2)[1].split("?", 1)[1].split("&")
        params = {
            key:
            value for (key, value) in [
                token.split("=") for token in param_tokens
            ]
        }

        # Check for an authorisation failure
        check_failure(client, params, state)

        refresh_token = reddit.auth.authorize(params["code"])
        add_refresh_token(creds, refresh_token)
        send_message(
            client,
            f"Refresh token: {refresh_token}. Feel free to close"
            + " this page. This message is simply for success"
            + " confirmation; it is not necessary to save your"
            + " refresh_token, as TCF has automatically done this.",
        )
    # If user has already authorised TCF
    else:
        reddit = praw.Reddit(refresh_token=creds["refresh_token"], **constants)

    return reddit


def receive_connection() -> object:
    """Wait for and then return a connected socket. Opens a TCP connection on port 8080,
    and waits for a single client.

    :return: A client object.
    """
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind(("localhost", 8080))
    server.listen(1)
    client = server.accept()[0]
    server.close()
    return client


def send_message(client: object, message: str) -> None:
    """Sends a message to the client and closes the connection.

    :client:   A web client object.
    :message:  The message to send/
    """
    client.send(f"HTTP/1.1 200 OK\r\n\r\n{message}".encode("utf-8"))
    client.close()