#!/usr/bin/env python3

import argparse
import glob
import importlib
import sys
from pathlib import Path
from typing import Dict, List


TABLE_OFFSET = 10
COLUMN_WIDTHS = (23, 20, 88)  # Use 88 as line-length for docstring for simplicity
SPACEBAR = " "

# leaving blank rows after exhausting files from each folder
BLANK_ROW = f"| {SPACEBAR:{COLUMN_WIDTHS[0]}}| {SPACEBAR:{COLUMN_WIDTHS[1]}}| {SPACEBAR:{COLUMN_WIDTHS[2]}}|\n"

# absolute path to zulip-terminal
ROOT_DIRECTORY = Path(__file__).resolve().parent.parent

# absolute path to zulip-terminal/zulipterminal to be passed as parameter
ZULIPTERMINAL = ROOT_DIRECTORY / "zulipterminal"

# new doc file has been created for the time being to compare it with the original
DEVELOPER_DOC_NAME = "developer-file-overview.md"
DEVELOPER_DOC_PATH = ROOT_DIRECTORY / "docs" / DEVELOPER_DOC_NAME

# Documentation for these folders is incomplete or excluded, so is specified here instead
DESC_FOR_NO_FILE_FOLDERS = {
    "zulipterminal/themes": "Themes bundled with the application",
    "zulipterminal/scripts": "Scripts bundled with the application",
}

# Top-level folder names to exclude, unrelated to the source
FOLDERS_TO_EXCLUDE = ["__pycache__"]


def main(fix_file: bool) -> None:
    if fix_file:
        create_file_overview_doc()
        print(f"Generated {DEVELOPER_DOC_NAME} successfully.")
    else:
        error_messages = lint_file_overview()
        if error_messages:
            for file_name, error in error_messages.items():
                print(f"{file_name}: {error} ")
            print(
                f"\nRun './tools/lint-docstring --fix' to add the docstring to {DEVELOPER_DOC_NAME}"
            )
            sys.exit(1)
        else:
            print(f"{DEVELOPER_DOC_NAME} has been linted successfully.")


def lint_file_overview() -> Dict[str, str]:
    """
    Used for linting of the developer-file-overview.md document
    """
    folder_file_docstring = generate_folder_file_docstrings_dict()
    existing_doc_dict = extract_docstrings_from_file_overview()
    # folders and files which return error while linting are stored under
    # error_message_dict with their respective error messages
    error_message_dict = {}
    for folder in folder_file_docstring:
        # check if the folder for the docstring is present or not
        if folder in existing_doc_dict:
            for file in folder_file_docstring[folder]:
                # check if the file under the folder is present or not
                if file in existing_doc_dict[folder]:
                    # check if docstrings match or not
                    if (
                        folder_file_docstring[folder][file]
                        != existing_doc_dict[folder][file]
                    ):
                        error_message_dict[
                            f"{folder}/{file}"
                        ] = f"Docstrings do not match those listed in {DEVELOPER_DOC_NAME}"
                    del existing_doc_dict[folder][file]
                else:
                    error_message_dict[
                        f"{folder}/{file}"
                    ] = f"File does not exist in {DEVELOPER_DOC_NAME}"
            # if the folder dictionary is empty, delete it from the existing_doc_dict
            if not existing_doc_dict[folder]:
                del existing_doc_dict[folder]
        else:
            error_message_dict[folder] = f"Folder not present in {DEVELOPER_DOC_NAME}"

    # for docstrings in DESC_FOR_NO_FILE_FOLDERS
    for folder_name in DESC_FOR_NO_FILE_FOLDERS:
        if (
            DESC_FOR_NO_FILE_FOLDERS[folder_name]
            != existing_doc_dict[folder_name]["no_file_present"]
        ):
            error_message_dict[
                folder_name
            ] = f"Docstrings do not match those listed in {DEVELOPER_DOC_NAME}"
        del existing_doc_dict[folder_name]

    # check if there are any docstrings in the overview file
    # for which folder/file has been removed
    if existing_doc_dict:
        for folder_name in existing_doc_dict:
            if existing_doc_dict[folder_name]:
                for file_name in existing_doc_dict[folder_name]:
                    error_message_dict[
                        f"{folder_name}/{file_name}"
                    ] = "Folder/File for this docstring has been removed"
    return error_message_dict


def create_file_overview_doc() -> None:
    """
    Recreates the document by retaining the lines 1 to TABLE_OFFSET,
    and the rest is written using the docstrings in the files
    """
    folder_file_docstring = generate_folder_file_docstrings_dict()

    table_markdown = []
    for folder in folder_file_docstring:
        dictionary_of_files = folder_file_docstring[folder]

        folder_text = folder
        for file in sorted(dictionary_of_files):
            new_row = f"| {folder_text:{COLUMN_WIDTHS[0]}}| {file:{COLUMN_WIDTHS[1]}}| {folder_file_docstring[folder][file]:{COLUMN_WIDTHS[2]}}|\n"
            table_markdown.append(new_row)
            folder_text = " "

        # adding blank row at the end of every folder
        table_markdown.append(BLANK_ROW)

    # Folders that do not contain any files with docstrings are added separately to the file-overview
    for folder_name in sorted(DESC_FOR_NO_FILE_FOLDERS):
        new_row = f"| {folder_name:{COLUMN_WIDTHS[0]}}| {SPACEBAR:{COLUMN_WIDTHS[1]}}| {DESC_FOR_NO_FILE_FOLDERS[folder_name]:{COLUMN_WIDTHS[2]}}|\n"
        table_markdown.extend([new_row, BLANK_ROW])

    with open(DEVELOPER_DOC_PATH, "r") as dev_file:
        doc_data = dev_file.readlines()

    doc_data[TABLE_OFFSET - 1 :] = table_markdown[:-1]
    updated_data = "".join(doc_data)

    with open(DEVELOPER_DOC_PATH, "w") as dev_file:
        dev_file.write(updated_data)


def generate_folder_file_docstrings_dict() -> Dict[str, Dict[str, str]]:
    """
    Returns a dictionary containing folder name which in turn
    is a dictionary containing files and their respective descriptions
    """
    total_files = extract_folder_file_structure()

    folder_file_docstring: Dict[str, Dict[str, str]] = {}
    for folder, files in sorted(total_files.items()):
        folder_file_docstring[str(folder)] = {}
        for file in files:
            imported_file = importlib.import_module(
                f'{folder.replace("/",".")}.{file[:-3]}'
            )
            extracted_docstring = str(imported_file.__doc__)
            docstring = extracted_docstring.strip().replace("\n", " ")
            if len(docstring) > COLUMN_WIDTHS[2]:
                print(
                    f"ERROR: {file} has docstring longer than maximum {COLUMN_WIDTHS[2]}"
                )
                sys.exit(1)
            folder_file_docstring[str(folder)][file] = docstring
    return folder_file_docstring


def extract_folder_file_structure() -> Dict[str, List[str]]:
    """
    Returns dictionary containing folders and respective python files within them
    """
    folders_and_files = {}
    for path_to_folder in glob.glob(f"{ZULIPTERMINAL}/**/", recursive=True):
        complete_directory_path = Path(path_to_folder)
        if complete_directory_path.name in FOLDERS_TO_EXCLUDE:
            continue
        relative_directory_path = complete_directory_path.relative_to(ROOT_DIRECTORY)
        if str(relative_directory_path) not in DESC_FOR_NO_FILE_FOLDERS:
            files_in_directory = [
                file.name
                for file in complete_directory_path.glob("*.py")
                if file.name != "__init__.py"
            ]
            folders_and_files[str(relative_directory_path)] = files_in_directory
    return folders_and_files


def extract_docstrings_from_file_overview() -> Dict[str, Dict[str, str]]:
    """
    Reads the developer-file-overview.md document and creates
    a dictionary containing folder name which in turn is a dictionary
    containing files and their respective descriptions
    """
    with open(DEVELOPER_DOC_PATH, "r") as file:
        doc_data = file.readlines()

    existing_doc_dict: Dict[str, Dict[str, str]] = {}

    # table is present from line TABLE_OFFSET to (length - 1)
    for doc_row in doc_data[TABLE_OFFSET - 1 :]:
        _, folder, filename, docstring, _ = doc_row.split("|")
        folder, filename = folder.strip(), filename.strip()
        if folder:
            # For files under a folder but no folder value in overview file (eg. core.py)
            folder_name = folder
            existing_doc_dict[str(folder_name)] = {}
        if docstring.strip():
            if filename:
                existing_doc_dict[folder_name][filename] = docstring.strip()
            else:
                existing_doc_dict[folder_name]["no_file_present"] = docstring.strip()
    return existing_doc_dict


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description=f"Lint {DEVELOPER_DOC_NAME} as per docstrings across all files in zulipterminal"
    )
    parser.add_argument(
        "--fix",
        action="store_true",
        help=f"Regenerate {DEVELOPER_DOC_NAME} according to the docstrings",
    )
    args = parser.parse_args()
    main(args.fix)
