from typing import TypedDict

from aiohttp import ClientConnectorError, ClientSession

from pydis_core.utils import logging

log = logging.get_logger(__name__)

DEFAULT_PASTEBIN = "https://paste.pythondiscord.com"
FAILED_REQUEST_ATTEMPTS = 3
MAX_PASTE_SIZE = 128 * 1024  # 128kB
"""The maximum allows size of a paste, in bytes."""
SUPPORTED_LEXERS = [
    "abap",
    "abnf",
    "actionscript",
    "actionscript3",
    "ada",
    "adl",
    "agda",
    "aheui",
    "alloy",
    "ambienttalk",
    "amdgpu",
    "ampl",
    "ansys",
    "antlr",
    "antlr-actionscript",
    "antlr-cpp",
    "antlr-csharp",
    "antlr-java",
    "antlr-objc",
    "antlr-perl",
    "antlr-python",
    "antlr-ruby",
    "apacheconf",
    "apl",
    "applescript",
    "arduino",
    "arrow",
    "arturo",
    "asc",
    "aspectj",
    "aspx-cs",
    "aspx-vb",
    "asymptote",
    "augeas",
    "autodetect",
    "autohotkey",
    "autoit",
    "awk",
    "bare",
    "basemake",
    "bash",
    "batch",
    "bbcbasic",
    "bbcode",
    "bc",
    "bdd",
    "befunge",
    "berry",
    "bibtex",
    "blitzbasic",
    "blitzmax",
    "bnf",
    "boa",
    "boo",
    "boogie",
    "brainfuck",
    "bst",
    "bugs",
    "c",
    "c-objdump",
    "ca65",
    "cadl",
    "camkes",
    "capdl",
    "capnp",
    "carbon",
    "cbmbas",
    "cddl",
    "ceylon",
    "cfc",
    "cfengine3",
    "cfm",
    "cfs",
    "chaiscript",
    "chapel",
    "charmci",
    "cheetah",
    "cirru",
    "clay",
    "clean",
    "clojure",
    "clojurescript",
    "cmake",
    "cobol",
    "cobolfree",
    "coffeescript",
    "comal",
    "common-lisp",
    "componentpascal",
    "console",
    "coq",
    "cplint",
    "cpp",
    "cpp-objdump",
    "cpsa",
    "cr",
    "crmsh",
    "croc",
    "cryptol",
    "csharp",
    "csound",
    "csound-document",
    "csound-score",
    "css",
    "css+django",
    "css+genshitext",
    "css+lasso",
    "css+mako",
    "css+mozpreproc",
    "css+myghty",
    "css+php",
    "css+ruby",
    "css+smarty",
    "css+ul4",
    "cuda",
    "cypher",
    "cython",
    "d",
    "d-objdump",
    "dart",
    "dasm16",
    "dax",
    "debcontrol",
    "debsources",
    "delphi",
    "devicetree",
    "dg",
    "diff",
    "django",
    "docker",
    "doscon",
    "dpatch",
    "dtd",
    "duel",
    "dylan",
    "dylan-console",
    "dylan-lid",
    "earl-grey",
    "easytrieve",
    "ebnf",
    "ec",
    "ecl",
    "eiffel",
    "elixir",
    "elm",
    "elpi",
    "emacs-lisp",
    "email",
    "erb",
    "erl",
    "erlang",
    "evoque",
    "execline",
    "extempore",
    "ezhil",
    "factor",
    "fan",
    "fancy",
    "felix",
    "fennel",
    "fift",
    "fish",
    "flatline",
    "floscript",
    "forth",
    "fortran",
    "fortranfixed",
    "foxpro",
    "freefem",
    "fsharp",
    "fstar",
    "func",
    "futhark",
    "gap",
    "gap-console",
    "gas",
    "gcode",
    "gdscript",
    "genshi",
    "genshitext",
    "gherkin",
    "glsl",
    "gnuplot",
    "go",
    "golo",
    "gooddata-cl",
    "gosu",
    "graphviz",
    "groff",
    "groovy",
    "gsql",
    "gst",
    "haml",
    "handlebars",
    "haskell",
    "haxe",
    "haxeml",
    "hexdump",
    "hlsl",
    "hsail",
    "hspec",
    "html",
    "html+cheetah",
    "html+django",
    "html+evoque",
    "html+genshi",
    "html+handlebars",
    "html+lasso",
    "html+mako",
    "html+myghty",
    "html+ng2",
    "html+php",
    "html+smarty",
    "html+twig",
    "html+ul4",
    "html+velocity",
    "http",
    "hybris",
    "hylang",
    "i6t",
    "icon",
    "idl",
    "idris",
    "iex",
    "igor",
    "inform6",
    "inform7",
    "ini",
    "io",
    "ioke",
    "irc",
    "isabelle",
    "j",
    "jags",
    "jasmin",
    "java",
    "javascript",
    "javascript+cheetah",
    "javascript+django",
    "javascript+lasso",
    "javascript+mako",
    "javascript+mozpreproc",
    "javascript+myghty",
    "javascript+php",
    "javascript+ruby",
    "javascript+smarty",
    "jcl",
    "jlcon",
    "jmespath",
    "js+genshitext",
    "js+ul4",
    "jsgf",
    "jslt",
    "json",
    "jsonld",
    "jsonnet",
    "jsp",
    "julia",
    "juttle",
    "k",
    "kal",
    "kconfig",
    "kmsg",
    "koka",
    "kotlin",
    "kuin",
    "lasso",
    "lean",
    "less",
    "lighttpd",
    "lilypond",
    "limbo",
    "liquid",
    "literate-agda",
    "literate-cryptol",
    "literate-haskell",
    "literate-idris",
    "livescript",
    "llvm",
    "llvm-mir",
    "llvm-mir-body",
    "logos",
    "logtalk",
    "lsl",
    "lua",
    "macaulay2",
    "make",
    "mako",
    "maql",
    "markdown",
    "mask",
    "mason",
    "mathematica",
    "matlab",
    "matlabsession",
    "maxima",
    "mcfunction",
    "mcschema",
    "meson",
    "mime",
    "minid",
    "miniscript",
    "mips",
    "modelica",
    "modula2",
    "monkey",
    "monte",
    "moocode",
    "moonscript",
    "mosel",
    "mozhashpreproc",
    "mozpercentpreproc",
    "mql",
    "mscgen",
    "mupad",
    "mxml",
    "myghty",
    "mysql",
    "nasm",
    "ncl",
    "nemerle",
    "nesc",
    "nestedtext",
    "newlisp",
    "newspeak",
    "ng2",
    "nginx",
    "nimrod",
    "nit",
    "nixos",
    "nodejsrepl",
    "notmuch",
    "nsis",
    "numpy",
    "nusmv",
    "objdump",
    "objdump-nasm",
    "objective-c",
    "objective-c++",
    "objective-j",
    "ocaml",
    "octave",
    "odin",
    "omg-idl",
    "ooc",
    "opa",
    "openedge",
    "output",
    "pacmanconf",
    "pan",
    "parasail",
    "pawn",
    "peg",
    "perl",
    "perl6",
    "phix",
    "php",
    "pig",
    "pike",
    "pkgconfig",
    "plpgsql",
    "pointless",
    "pony",
    "portugol",
    "postgres-explain",
    "postgresql",
    "postscript",
    "pot",
    "pov",
    "powershell",
    "praat",
    "procfile",
    "prolog",
    "promql",
    "properties",
    "protobuf",
    "psql",
    "psysh",
    "pug",
    "puppet",
    "pwsh-session",
    "py+ul4",
    "py2tb",
    "pycon",
    "pypylog",
    "pytb",
    "python",
    "python2",
    "q",
    "qbasic",
    "qlik",
    "qml",
    "qvto",
    "racket",
    "ragel",
    "ragel-c",
    "ragel-cpp",
    "ragel-d",
    "ragel-em",
    "ragel-java",
    "ragel-objc",
    "ragel-ruby",
    "rbcon",
    "rconsole",
    "rd",
    "reasonml",
    "rebol",
    "red",
    "redcode",
    "registry",
    "resourcebundle",
    "restructuredtext",
    "rexx",
    "rhtml",
    "ride",
    "rita",
    "rng-compact",
    "roboconf-graph",
    "roboconf-instances",
    "robotframework",
    "rql",
    "rsl",
    "ruby",
    "rust",
    "sarl",
    "sas",
    "sass",
    "savi",
    "scala",
    "scaml",
    "scdoc",
    "scheme",
    "scilab",
    "scss",
    "sed",
    "sgf",
    "shen",
    "shexc",
    "sieve",
    "silver",
    "singularity",
    "slash",
    "slim",
    "slurm",
    "smali",
    "smalltalk",
    "smarty",
    "smithy",
    "sml",
    "snbt",
    "snobol",
    "snowball",
    "solidity",
    "sophia",
    "sp",
    "sparql",
    "spec",
    "spice",
    "splus",
    "sql",
    "sql+jinja",
    "sqlite3",
    "squidconf",
    "srcinfo",
    "ssp",
    "stan",
    "stata",
    "supercollider",
    "swift",
    "swig",
    "systemverilog",
    "tads3",
    "tal",
    "tap",
    "tasm",
    "tcl",
    "tcsh",
    "tcshcon",
    "tea",
    "teal",
    "teratermmacro",
    "termcap",
    "terminfo",
    "terraform",
    "tex",
    "text",
    "thrift",
    "ti",
    "tid",
    "tlb",
    "tnt",
    "todotxt",
    "toml",
    "trac-wiki",
    "trafficscript",
    "treetop",
    "tsql",
    "turtle",
    "twig",
    "typescript",
    "typoscript",
    "typoscriptcssdata",
    "typoscripthtmldata",
    "ucode",
    "ul4",
    "unicon",
    "unixconfig",
    "urbiscript",
    "usd",
    "vala",
    "vb.net",
    "vbscript",
    "vcl",
    "vclsnippets",
    "vctreestatus",
    "velocity",
    "verilog",
    "vgl",
    "vhdl",
    "vim",
    "wast",
    "wdiff",
    "webidl",
    "wgsl",
    "whiley",
    "wikitext",
    "wowtoc",
    "wren",
    "x10",
    "xml",
    "xml+cheetah",
    "xml+django",
    "xml+evoque",
    "xml+lasso",
    "xml+mako",
    "xml+myghty",
    "xml+php",
    "xml+ruby",
    "xml+smarty",
    "xml+ul4",
    "xml+velocity",
    "xorg.conf",
    "xpp",
    "xquery",
    "xslt",
    "xtend",
    "xul+mozpreproc",
    "yaml",
    "yaml+jinja",
    "yang",
    "zeek",
    "zephir",
    "zig",
]
"""Lexers supported by the default pastebin."""


class PasteResponse(TypedDict):
    """
    A successful response from the paste service.

    args:
        link: The URL to the saved paste.
        removal: The URL to delete the saved paste.
    """

    link: str
    removal: str


class PasteUploadError(Exception):
    """Raised when an error is encountered uploading to the paste service."""

class PasteUnsupportedLexerError(Exception):
    """Raised when an unsupported lexer is used."""


class PasteTooLongError(Exception):
    """Raised when content is too large to upload to the paste service."""


async def send_to_paste_service(
    *,
    contents: str,
    http_session: ClientSession,
    file_name: str = "",
    lexer: str = "python",
    paste_url: str = DEFAULT_PASTEBIN,
    max_size: int = MAX_PASTE_SIZE,
) -> PasteResponse:
    """
    Upload some contents to the paste service.

    Args:
        contents: The content to upload to the paste service.
        http_session (aiohttp.ClientSession): The session to use when POSTing the content to the paste service.
        file_name: The name of the file to save to the paste service.
        lexer: The lexer to save the content with.
        paste_url: The base url to the paste service.
        max_size: The max number of bytes to be allowed. Anything larger than :obj:`MAX_PASTE_SIZE` will be rejected.

    Raises:
        :exc:`ValueError`: ``max_length`` greater than the maximum allowed by the paste service.
        :exc:`PasteTooLongError`: ``contents`` too long to upload.
        :exc:`PasteUploadError`: Uploading failed.

    Returns:
        A :obj:`TypedDict` containing both the URL of the paste, and a URL to remove the paste.
    """
    if max_size > MAX_PASTE_SIZE:
        raise ValueError(f"`max_length` must not be greater than {MAX_PASTE_SIZE}")

    if lexer not in SUPPORTED_LEXERS:
        if paste_url == DEFAULT_PASTEBIN:
            raise PasteUnsupportedLexerError(f"Lexer '{lexer}' not supported by pastebin.")

        try:
            async with http_session.get(f"{paste_url}/api/v1/lexer") as response:
                response_json = await response.json()  # Supported lexers are the keys.
        except Exception:
            raise PasteUploadError("Could not fetch supported lexers from selected paste_url.")
        if lexer not in response_json:
            raise PasteUnsupportedLexerError(f"Lexer '{lexer}' not supported by pastebin.")

    contents_size = len(contents.encode())
    if contents_size > max_size:
        log.info("Contents too large to send to paste service.")
        raise PasteTooLongError(f"Contents of size {contents_size} greater than maximum size {max_size}")

    log.debug(f"Sending contents of size {contents_size} bytes to paste service.")
    payload = {
        "expiry": "1month",
        "long": "on",  # Use a longer URI for the paste.
        "files": [
            {"name": file_name, "lexer": lexer, "content": contents},
        ]
    }
    for attempt in range(1, FAILED_REQUEST_ATTEMPTS + 1):
        try:
            async with http_session.post(f"{paste_url}/api/v1/paste", json=payload) as response:
                response_json = await response.json()
        except ClientConnectorError:
            log.warning(
                f"Failed to connect to paste service at url {paste_url}, "
                f"trying again ({attempt}/{FAILED_REQUEST_ATTEMPTS})."
            )
            continue
        except Exception:
            log.exception(
                f"An unexpected error has occurred during handling of the request, "
                f"trying again ({attempt}/{FAILED_REQUEST_ATTEMPTS})."
            )
            continue

        if response.status == 400:
            log.warning(
                f"Paste service returned error {response_json['message']} with status code {response.status}, "
                f"trying again ({attempt}/{FAILED_REQUEST_ATTEMPTS})."
            )
            continue

        if response.status == 200:
            log.info(f"Successfully uploaded contents to {response_json['link']}.")
            return PasteResponse(link=response_json["link"], removal=response_json["removal"])

        log.warning(
            f"Got unexpected JSON response from paste service: {response_json}\n"
            f"trying again ({attempt}/{FAILED_REQUEST_ATTEMPTS})."
        )

    raise PasteUploadError("Failed to upload contents to paste service")
