/*
 * Author: Ruben Fiszel
 * Copyright: Windmill Labs, Inc 2022
 * This file and its contents are licensed under the AGPLv3 License.
 * Please see the included NOTICE for copyright information and
 * LICENSE-AGPL for a copy of the license.
 */

mod mapping;

#[cfg(not(target_arch = "wasm32"))]
use async_recursion::async_recursion;
use itertools::Itertools;
use lazy_static::lazy_static;
#[cfg(not(target_arch = "wasm32"))]
use std::str::FromStr;
#[cfg(not(target_arch = "wasm32"))]
use std::collections::HashMap;

use mapping::{FULL_IMPORTS_MAP, SHORT_IMPORTS_MAP};
#[cfg(not(target_arch = "wasm32"))]
use regex::Regex;
#[cfg(target_arch = "wasm32")]
use regex_lite::Regex;

use rustpython_parser::{
    ast::{Stmt, StmtImport, StmtImportFrom, Suite},
    text_size::TextRange,
    Parse,
};
#[cfg(not(target_arch = "wasm32"))]
use sqlx::{Pool, Postgres};
#[cfg(not(target_arch = "wasm32"))]
use windmill_common::{
    error::{self, to_anyhow},
    worker::{
        split_python_requirements, try_parse_locked_python_version_from_requirements,
        PythonAnnotations,
    },
    workspace_dependencies::{RawWorkspaceDependencies, WorkspaceDependenciesPrefetched},
};

fn replace_import(x: String) -> String {
    SHORT_IMPORTS_MAP
        .get(&x)
        .map(|x| x.to_owned())
        .unwrap_or(&x)
        .to_string()
}

fn replace_full_import(x: &str) -> Option<String> {
    FULL_IMPORTS_MAP.get(x).map(|x| (*x).to_owned())
}

#[cfg(not(target_arch = "wasm32"))]
lazy_static! {
    static ref RE: Regex = Regex::new(r"^\#\s?(\S+)\s*$").unwrap();
    static ref PKG_RE: Regex = Regex::new(r"^([^!=<>]+)(?:[!=<>]|$)").unwrap();
}

lazy_static! {
    static ref PIN_RE: Regex = Regex::new(r"(?:\s*#\s*(pin|repin):\s*)(\S*)").unwrap();
    // Regex to properly match main function definition at line start,
    // capturing both sync and async variants
    static ref DEF_MAIN_RE: Regex = Regex::new(r"(?m)^(async\s+)?def\s+main\s*\(").unwrap();
}

fn process_import(module: Option<String>, path: &str, level: usize) -> Vec<NImport> {
    if level > 0 {
        let mut imports = vec![];
        let splitted_path = path.split("/");
        let base = splitted_path
            .clone()
            .take(splitted_path.count() - level)
            .join("/");
        if let Some(m) = module {
            imports.push(NImport::Relative(format!("{base}/{}", m.replace(".", "/"))));
        } else {
            imports.push(NImport::Relative(format!("{base}")));
        }
        imports
    } else if let Some(module) = module {
        let imprt = module.split('.').next().unwrap_or("").replace("_", "-");
        if imprt == "u" || imprt == "f" {
            vec![NImport::Relative(module.replace(".", "/"))]
        } else {
            let pkg = replace_full_import(&module).unwrap_or(replace_import(imprt));
            vec![NImport::Auto { key: if module == pkg { None } else { Some(module) }, pkg }]
        }
    } else {
        vec![]
    }
}

pub fn parse_relative_imports(code: &str, path: &str) -> anyhow::Result<Vec<String>> {
    let nimports = parse_code_for_imports(code, path)?;
    return Ok(nimports
        .into_iter()
        .filter_map(|x| match x {
            NImport::Relative(path) => Some(path),
            _ => None,
        })
        .collect());
}

#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum NImport {
    // Order matters! First we want to resolve all repins

    // manually repinned requirement
    // e.g.:
    // import pandas # repin: pandas==x.y.z
    Repin {
        pin: ImportPin,
        key: String,
    },
    // manually pinned requirements
    // e.g.:
    // import pandas # pin: pandas>=x.y.z
    // import pandas # pin: pandas<=x.y.z
    //
    // NOTE: It is possible for multiple pins exist on same import
    // That's why we store vector of pins
    Pin {
        pins: Vec<ImportPin>,
        key: String,
    },
    // Automatically inferred requirement
    // e.g.:
    // import pandas
    Auto {
        // Take `x.y.z` for example
        // x is going to be the `root`
        // and x.y.z is `full`
        //
        // `full` will be None if it is equal to root
        //
        // We will use `root` as a requirement name and pass to `uv pip compile` if it was not replaced with any pin
        pkg: String,

        // However we still need full, since all pins pin against full import names
        key: Option<String>,
    },
    // Relative imports
    Relative(String),
}

#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
enum NImportResolved {
    Repin { pin: ImportPin, key: String },
    Pin { pins: Vec<ImportPin>, key: String },
    Auto { pkg: String, key: Option<String> },
}

#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ImportPin {
    pub pkg: String,
    pub path: String,
}

pub fn parse_code_for_imports(code: &str, path: &str) -> anyhow::Result<Vec<NImport>> {
    // Use regex to safely find the main function definition
    let mut code = DEF_MAIN_RE
        .split(code)
        .next()
        .unwrap_or_default()
        .to_string();

    // remove main function decorator from end of file if it exists
    if code
        .lines()
        .last()
        .map(|x| x.starts_with("@"))
        .unwrap_or(false)
    {
        code = code
            .lines()
            .take(code.lines().count() - 1)
            .collect::<Vec<&str>>()
            .join("\n")
            + "\n";
    }

    // Add a fake main function to ensure the parser can process the code correctly
    // This is needed because we've split off the real main function above
    let code_with_fake_main = format!("{}\n\ndef main(): pass", code);

    let ast = Suite::parse(&code_with_fake_main, "main.py").map_err(|e| {
        anyhow::anyhow!("Error parsing code for imports: {}", e.to_string())
    })?;
    // Note: We're still using the original code for finding pins,
    // as the TextRange values from the parsed AST would be based on code_with_fake_main
    // but we want to match against the original code
    let find_pin = |range: TextRange, key: String| {
        let hs = code
            .chars()
            .skip(range.end().to_usize())
            .take_while(|e| *e != '\n')
            .collect::<String>();

        if hs.trim_start().is_empty() {
            return None;
        }

        PIN_RE.captures(&hs).and_then(|x| {
            x.get(1).zip(x.get(2)).and_then(|(ty_m, pkg_m)| {
                let pkg = pkg_m.as_str().to_owned();
                if ty_m.as_str() == "pin" {
                    Some(vec![NImport::Pin {
                        pins: vec![ImportPin { pkg, path: path.to_owned() }],
                        key,
                    }])
                } else if ty_m.as_str() == "repin" {
                    Some(vec![NImport::Repin {
                        pin: ImportPin { pkg, path: path.to_owned() },
                        key,
                    }])
                } else {
                    None
                }
            })
        })
    };

    let mut nimports: Vec<NImport> = ast
        .into_iter()
        .filter_map(|x| match x {
            Stmt::Import(StmtImport { names, range }) => names
                .get(0)
                .and_then(|al| find_pin(range, al.name.to_string()))
                .or(Some(
                    names
                        .into_iter()
                        .map(|x| {
                            let name = x.name.to_string();
                            process_import(Some(name), path, 0)
                        })
                        .flatten()
                        .collect::<Vec<NImport>>(),
                )),
            Stmt::ImportFrom(StmtImportFrom { level: Some(i), module, .. }) if i.to_u32() > 0 => {
                Some(process_import(
                    module.map(|x| x.to_string()),
                    path,
                    i.to_usize(),
                ))
            }
            Stmt::ImportFrom(StmtImportFrom { level: _, module, range, .. }) => find_pin(
                range,
                module.clone().map(|x| x.to_string()).unwrap_or_default(),
            )
            .or(Some(process_import(module.map(|x| x.to_string()), path, 0))),
            _ => None,
        })
        .flatten()
        .filter(|x| {
            if let NImport::Auto { ref pkg, .. } = x {
                !STDIMPORTS.contains(&(*pkg).as_str())
            } else {
                true
            }
        })
        .unique()
        .collect();

    nimports.sort();
    return Ok(nimports);
}

#[cfg(not(target_arch = "wasm32"))]
pub async fn parse_python_imports(
    code: &str,
    w_id: &str,
    path: &str,
    db: &Pool<Postgres>,
    version_specifiers: &mut Vec<pep440_rs::VersionSpecifier>,
    locked_v: &mut Option<pep440_rs::Version>,
    raw_workspace_dependencies_o: &Option<RawWorkspaceDependencies>,
    temp_script_refs: &Option<HashMap<String, String>>,
) -> error::Result<(Vec<String>, Option<String>)> {
    let mut compile_error_hint: Option<String> = None;
    let mut imports = parse_python_imports_inner(
        code,
        w_id,
        path,
        db,
        &mut vec![],
        version_specifiers,
        &mut None,
        locked_v,
        raw_workspace_dependencies_o,
        temp_script_refs,
    )
    .await?
    .into_values()
    .map(|nimport| match nimport {
        NImportResolved::Pin { pins, .. } => pins.into_iter().map(|p| {
            if let Some(hint) = &mut compile_error_hint{
                    hint.push_str(&format!("\n - pin to {} in {}", p.pkg, p.path));
            } else {
                compile_error_hint = Some("\n\nMultiple pins can cause problems during lockfile resolution.\nMake sure you checked every pin for conflicts:\n".into())
            };
            Ok(p.pkg)
        }).collect_vec(),
        NImportResolved::Repin { pin: ImportPin { pkg, .. }, .. } => vec![Ok(pkg)],
        NImportResolved::Auto { pkg, key } => vec![

            if let Some(key) = key {
               Ok(format!("{pkg} # (mapped from {key})"))
            } else {
               Ok(pkg)
            }
        ],
    })
    .flatten()
    .collect::<error::Result<Vec<String>>>()?
    .into_iter()
    .filter(|x| !x.trim_start().starts_with("--") && !x.trim().is_empty())
    .unique()
    .collect_vec();

    imports.sort();

    compile_error_hint
        .as_mut()
        .map(|e| e.push_str("\n\nNOTE: You can also `repin` to override all pins"));
    Ok((imports, compile_error_hint))
}

#[cfg(not(target_arch = "wasm32"))]
fn extract_pkg_name(requirement: &str) -> String {
    PKG_RE
        .captures(requirement)
        .map(|x| x.get(1).map(|m| m.as_str().to_string()).unwrap_or_default())
        .unwrap_or_default()
}

#[cfg(not(target_arch = "wasm32"))]
#[async_recursion]
async fn parse_python_imports_inner(
    code: &str,
    w_id: &str,
    path: &str,
    db: &Pool<Postgres>,
    already_visited: &mut Vec<String>,
    version_specifiers: &mut Vec<pep440_rs::VersionSpecifier>,
    path_where_annotated_pyv: &mut Option<String>,
    locked_v: &mut Option<pep440_rs::Version>,
    raw_workspace_dependencies_o: &Option<RawWorkspaceDependencies>,
    temp_script_refs: &Option<HashMap<String, String>>,
) -> error::Result<HashMap<String, NImportResolved>> {
    tracing::debug!("Parsing python imports for path: {}", path);
    let PythonAnnotations { py310, py311, py312, py313, .. } = PythonAnnotations::parse(&code);
    tracing::debug!(
        "Found python annotations - py310: {}, py311: {}, py312: {}, py313: {}",
        py310,
        py311,
        py312,
        py313
    );

    let mut push_version_specifiers = |perform, unparsed: String| -> error::Result<()> {
        if perform {
            tracing::debug!("Adding version specifier: {}", unparsed);
            pep440_rs::VersionSpecifiers::from_str(unparsed.as_str())
                .ok()
                .map(|vs| version_specifiers.extend(vs.to_vec()));
        }
        Ok(())
    };
    push_version_specifiers(py310, "==3.10.*".to_owned())?;
    push_version_specifiers(py311, "==3.11.*".to_owned())?;
    push_version_specifiers(py312, "==3.12.*".to_owned())?;
    push_version_specifiers(py313, "==3.13.*".to_owned())?;

    for x in code.lines() {
        if x.starts_with("# py:") || x.starts_with("#py:") {
            let version_spec = x.replace('#', "").replace("py:", "").trim().to_owned();
            tracing::debug!("Found inline python version specifier: {}", version_spec);
            push_version_specifiers(true, version_spec)?;
        } else if !x.starts_with('#') {
            break;
        }
    }
    // we pass only if there is none or only one annotation

    // Naive:
    // 1. Check if there are multiple annotated version
    // 2. If no, take one and compare with annotated version
    // 3. We continue if same or replace none with new one

    // Optimized:
    // 1. Iterate over all annotations compare each with annotated_pyv and replace on flight
    // 2. If annotated_pyv is different version, throw and error

    // This way we make sure there is no multiple annotations for same script
    // and we get detailed span on conflicting versions

    // #[derive(serde::Serialize, serde::Deserialize)]
    // struct InlineMetadata {
    //     requires_python: String,
    //     dependencies: Vec<String>,
    // }

    let mut final_imports = HashMap::new();
    tracing::debug!("Extracting workspace dependencies for workspace: {}", w_id);
    let wdp = WorkspaceDependenciesPrefetched::extract(
        code,
        windmill_common::scripts::ScriptLang::Python3,
        w_id,
        raw_workspace_dependencies_o,
        path,
        db.into(),
    )
    .await?;

    if let Some(c) = wdp.get_python()? {
        *locked_v = extract_nimports_from_content(&c, &mut final_imports);
    }

    if wdp.is_manual() {
        tracing::debug!(
            "Workspace dependencies mode is Manual, returning {} imports",
            final_imports.len()
        );
        return Ok(final_imports);
    }

    let find_requirements = code
        .lines()
        .find_position(|x| x.starts_with("# /// script"));
    tracing::debug!(
        "Looking for script metadata block, found: {}",
        find_requirements.is_some()
    );

    if let Some((pos, item)) = find_requirements {
        tracing::debug!("Found script metadata block at position {}: {}", pos, item);
        let mut requirements = HashMap::new();
        if item.starts_with("# /// script") {
            let mut incorrect = false;
            let metadata = code
                .lines()
                .skip(pos + 1)
                .map_while(|x| {
                    incorrect = !x.starts_with('#');
                    if incorrect || x.starts_with("# ///") {
                        Option::None
                    } else {
                        x.get(1..)
                    }
                })
                .join("\n")
                .parse::<toml::Table>()
                .map_err(to_anyhow)?;
            tracing::debug!("Parsed script metadata: {:?}", metadata);

            {
                if let Some(v) = metadata.get("requires-python").and_then(|v| v.as_str()) {
                    tracing::debug!("Found requires-python in metadata: {}", v);
                    push_version_specifiers(true, v.to_owned())?;
                }
            };

            metadata
                .get("dependencies")
                .and_then(|dependencies| dependencies.as_array())
                .inspect(|list| {
                    tracing::debug!("Found {} dependencies in script metadata", list.len());
                    for dependency_v in list.into_iter() {
                        let requirement = dependency_v.as_str().unwrap_or("ERROR").to_owned();
                        let key = extract_pkg_name(&requirement);
                        tracing::debug!(
                            "Adding dependency from metadata: {} (key: {})",
                            requirement,
                            key
                        );
                        requirements.insert(
                            key.clone(),
                            NImportResolved::Pin {
                                pins: vec![ImportPin {
                                    pkg: requirement.clone(),
                                    path: Default::default(),
                                }],
                                key,
                            },
                        );
                    }
                });
        }
        tracing::debug!(
            "Returning {} requirements from script metadata",
            requirements.len()
        );
        return Ok(requirements);
    }
    // Will get unsorted vector of imports found in current script
    let mut nimports = parse_code_for_imports(code, path)?;
    tracing::debug!("Found {} imports in code", nimports.len());

    // It is important to note, that sorting is important and will always result in this pattern:
    // 1. All Repins go first
    // 2. All Pins go second
    // 3. All Auto go third
    // 4. All relative imports go the last
    //
    // This way we make sure all repins are resolved before (re)pins inside imported relative scripts.
    nimports.sort();
    tracing::debug!("Processing imports in sorted order");

    for n in nimports.into_iter() {
        let mut nested = match n {
            NImport::Relative(rpath) => {
                // First try to get content from temp_script_refs cache if available
                let code_from_cache = if let Some(hash) = temp_script_refs.as_ref().and_then(|dt| dt.get(&rpath)) {
                    tracing::debug!("Found relative import '{}' in temp_script_refs with hash '{}'", rpath, hash);
                    match windmill_common::cache::raw_script_temp::load(hash.clone(), db).await {
                        Ok(content) => Some(content),
                        Err(e) => {
                            tracing::warn!("temp_script_refs hash '{}' not found in cache: {}, falling back to deployed script", hash, e);
                            None
                        }
                    }
                } else {
                    None
                };

                // Use cached content if available, otherwise fall back to deployed script
                let code = match code_from_cache {
                    Some(content) => content,
                    None => {
                        sqlx::query_scalar!(
                            r#"
                        SELECT content FROM script WHERE path = $1 AND workspace_id = $2
                        AND archived = false ORDER BY created_at DESC LIMIT 1
                        "#,
                            &rpath,
                            w_id
                        )
                        .fetch_optional(db)
                        .await?
                        .unwrap_or_else(|| "".to_string())
                    }
                };

                if already_visited.contains(&rpath) {
                    vec![]
                } else {
                    already_visited.push(rpath.clone());
                    // Because the algo goes depth first, this function will never return relative import
                    // This why we can safely assume later, that there is no relative imports
                    parse_python_imports_inner(
                        &code,
                        w_id,
                        &rpath,
                        db,
                        already_visited,
                        version_specifiers,
                        path_where_annotated_pyv,
                        locked_v,
                        raw_workspace_dependencies_o,
                        temp_script_refs,
                    )
                    .await?
                    .into_values()
                    .collect_vec()
                }
            }
            NImport::Repin { pin, key } => vec![NImportResolved::Repin { pin, key }],
            NImport::Pin { pins, key } => vec![NImportResolved::Pin { pins, key }],
            NImport::Auto { pkg, key } => vec![NImportResolved::Auto { pkg, key }],
        };

        // Nested should also be sorted for the same reason
        nested.sort();
        tracing::debug!("Processing {} nested imports", nested.len());

        // At this point there should be no NImport::Relative in `nested`
        for imp in nested {
            let key = match imp.clone() {
                NImportResolved::Pin { key, .. } => key,
                NImportResolved::Repin { key, .. } => key,
                NImportResolved::Auto { key, pkg } => key.unwrap_or(pkg),
            };
            tracing::debug!("Resolving import with key: {}", key);
            // Handled cases:
            //
            //  1.
            //  Error: Imported windmill scripts have different pins
            //
            //  auto
            //  ├── pin:2
            //  └── pin:1
            //
            //  Fix 1:
            //
            //  auto
            //  ├── pin:1
            //  └── pin:1
            //
            //  Fix 2:
            //
            //  repin:1
            //  ├── pin:2
            //  └── pin:1
            //
            //  2.
            //  Error: Imported windmill scripts have different pins
            //
            //  pin:2
            //  └── pin:1
            //
            //  Fix 1:
            //
            //  auto
            //  └── pin:1
            //
            //  Fix 2:
            //
            //  repin:2
            //  └── pin:1
            //
            //  3. repins allowed to be repinned again
            //
            //  repin:2
            //  └── repin:1
            //
            match imp.clone() {
                NImportResolved::Repin { .. } => {
                    if let Some(existing_import) = final_imports.get(&key) {
                        match existing_import {
                            // replace
                            p if matches!(
                                p,
                                NImportResolved::Pin { .. } | NImportResolved::Auto { .. }
                            ) =>
                            {
                                final_imports.insert(key, imp);
                            }
                            // do nothing (older repins have greater precedence)
                            NImportResolved::Repin { .. } => {}
                            // Should not be possible
                            _ => {
                                return Err(anyhow::anyhow!(
                                    "Internal error: cannot resolve requirement pins",
                                )
                                .into());
                            }
                        }
                    } else {
                        final_imports.insert(key, imp.clone());
                    }
                }
                NImportResolved::Pin { pins: new_pins, .. } => {
                    if let Some(existing_import) = final_imports.get_mut(&key) {
                        match existing_import {
                            // Check if pin is the same version, if same, do nothing, if not error
                            NImportResolved::Pin { pins: existing_pins, .. } => {
                                existing_pins.extend(new_pins)
                            }
                            // do nothing
                            NImportResolved::Repin { .. } => {}
                            // Replace with new pin
                            NImportResolved::Auto { .. } => {
                                final_imports.insert(key, imp);
                            }
                        }
                    } else {
                        final_imports.insert(key, imp.clone());
                    }
                }
                NImportResolved::Auto { .. } => {
                    if !final_imports.contains_key(&key) {
                        final_imports.insert(key, imp);
                    }
                }
            }
        }
    }
    tracing::debug!(
        "Finished processing imports, returning {} final imports",
        final_imports.len()
    );
    Ok(final_imports)
}

#[cfg(not(target_arch = "wasm32"))]
fn extract_nimports_from_content(
    content: &str,
    hm: &mut HashMap<String, NImportResolved>,
) -> Option<pep440_rs::Version> {
    let lines = split_python_requirements(content);
    let locked_version = try_parse_locked_python_version_from_requirements(&lines);
    for requirement in lines {
        let key = extract_pkg_name(&requirement);
        hm.insert(
            key.clone(),
            NImportResolved::Pin {
                pins: vec![ImportPin { pkg: requirement, path: Default::default() }],
                key,
            },
        );
    }
    locked_version
}

const STDIMPORTS: [&str; 303] = [
    "--future--",
    "-abc",
    "-aix-support",
    "-ast",
    "-asyncio",
    "-bisect",
    "-blake2",
    "-bootsubprocess",
    "-bz2",
    "-codecs",
    "-codecs-cn",
    "-codecs-hk",
    "-codecs-iso2022",
    "-codecs-jp",
    "-codecs-kr",
    "-codecs-tw",
    "-collections",
    "-collections-abc",
    "-compat-pickle",
    "-compression",
    "-contextvars",
    "-crypt",
    "-csv",
    "-ctypes",
    "-curses",
    "-curses-panel",
    "-datetime",
    "-dbm",
    "-decimal",
    "-elementtree",
    "-frozen-importlib",
    "-frozen-importlib-external",
    "-functools",
    "-gdbm",
    "-hashlib",
    "-heapq",
    "-imp",
    "-io",
    "-json",
    "-locale",
    "-lsprof",
    "-lzma",
    "-markupbase",
    "-md5",
    "-msi",
    "-multibytecodec",
    "-multiprocessing",
    "-opcode",
    "-operator",
    "-osx-support",
    "-overlapped",
    "-pickle",
    "-posixshmem",
    "-posixsubprocess",
    "-py-abc",
    "-pydecimal",
    "-pyio",
    "-queue",
    "-random",
    "-sha1",
    "-sha256",
    "-sha3",
    "-sha512",
    "-signal",
    "-sitebuiltins",
    "-socket",
    "-sqlite3",
    "-sre",
    "-ssl",
    "-stat",
    "-statistics",
    "-string",
    "-strptime",
    "-struct",
    "-symtable",
    "-thread",
    "-threading-local",
    "-tkinter",
    "-tracemalloc",
    "-uuid",
    "-warnings",
    "-weakref",
    "-weakrefset",
    "-winapi",
    "-zoneinfo",
    "zoneinfo",
    "abc",
    "aifc",
    "antigravity",
    "argparse",
    "array",
    "ast",
    "asynchat",
    "asyncio",
    "asyncore",
    "atexit",
    "audioop",
    "base64",
    "bdb",
    "binascii",
    "binhex",
    "bisect",
    "builtins",
    "bz2",
    "cProfile",
    "calendar",
    "cgi",
    "cgitb",
    "chunk",
    "cmath",
    "cmd",
    "code",
    "codecs",
    "codeop",
    "collections",
    "colorsys",
    "compileall",
    "concurrent",
    "configparser",
    "contextlib",
    "contextvars",
    "copy",
    "copyreg",
    "crypt",
    "csv",
    "ctypes",
    "curses",
    "dataclasses",
    "datetime",
    "dbm",
    "decimal",
    "difflib",
    "dis",
    "distutils",
    "doctest",
    "email",
    "encodings",
    "ensurepip",
    "enum",
    "errno",
    "faulthandler",
    "fcntl",
    "filecmp",
    "fileinput",
    "fnmatch",
    "fractions",
    "ftplib",
    "functools",
    "gc",
    "genericpath",
    "getopt",
    "getpass",
    "gettext",
    "glob",
    "graphlib",
    "grp",
    "gzip",
    "hashlib",
    "heapq",
    "hmac",
    "html",
    "http",
    "idlelib",
    "imaplib",
    "imghdr",
    "imp",
    "importlib",
    "inspect",
    "io",
    "ipaddress",
    "itertools",
    "json",
    "keyword",
    "lib2to3",
    "linecache",
    "locale",
    "logging",
    "lzma",
    "mailbox",
    "mailcap",
    "marshal",
    "math",
    "mimetypes",
    "mmap",
    "modulefinder",
    "msilib",
    "msvcrt",
    "multiprocessing",
    "netrc",
    "nis",
    "nntplib",
    "nt",
    "ntpath",
    "nturl2path",
    "numbers",
    "opcode",
    "operator",
    "optparse",
    "os",
    "ossaudiodev",
    "pathlib",
    "pdb",
    "pickle",
    "pickletools",
    "pipes",
    "pkgutil",
    "platform",
    "plistlib",
    "poplib",
    "posix",
    "posixpath",
    "pprint",
    "profile",
    "pstats",
    "pty",
    "pwd",
    "py-compile",
    "pyclbr",
    "pydoc",
    "pydoc-data",
    "pyexpat",
    "queue",
    "quopri",
    "random",
    "re",
    "readline",
    "reprlib",
    "resource",
    "rlcompleter",
    "runpy",
    "sched",
    "secrets",
    "select",
    "selectors",
    "shelve",
    "shlex",
    "shutil",
    "signal",
    "site",
    "smtpd",
    "smtplib",
    "sndhdr",
    "socket",
    "socketserver",
    "spwd",
    "sqlite3",
    "sre-compile",
    "sre-constants",
    "sre-parse",
    "ssl",
    "stat",
    "statistics",
    "string",
    "stringprep",
    "struct",
    "subprocess",
    "sunau",
    "symtable",
    "sys",
    "sysconfig",
    "syslog",
    "tabnanny",
    "tarfile",
    "telnetlib",
    "tempfile",
    "termios",
    "textwrap",
    "this",
    "threading",
    "time",
    "timeit",
    "tkinter",
    "token",
    "tokenize",
    "trace",
    "traceback",
    "tracemalloc",
    "tty",
    "turtle",
    "turtledemo",
    "types",
    "typing",
    "unicodedata",
    "unittest",
    "urllib",
    "uu",
    "uuid",
    "venv",
    "warnings",
    "wave",
    "weakref",
    "webbrowser",
    "winreg",
    "winsound",
    "wsgiref",
    "xdrlib",
    "xml",
    "xmlrpc",
    "zipapp",
    "zipfile",
    "zipimport",
    "zlib",
    "",
];
