#!/usr/bin/env python3
"""Generate boilerplate for a new rule.

Example usage:

    python scripts/add_rule.py \
        --name PreferListBuiltin \
        --code PIE807 \
        --linter flake8-pie
"""

import argparse

from _utils import ROOT_DIR, dir_name, get_indent, pascal_case


def snake_case(name: str) -> str:
    """Convert from PascalCase to snake_case."""
    return "".join(
        f"_{word.lower()}" if word.isupper() else word for word in name
    ).lstrip("_")


def main(*, name: str, code: str, linter: str) -> None:
    """Generate boilerplate for a new rule."""
    # Create a test fixture.
    with (
        ROOT_DIR
        / "crates/ruff/resources/test/fixtures"
        / dir_name(linter)
        / f"{code}.py"
    ).open(
        "a",
    ):
        pass

    plugin_module = ROOT_DIR / "crates/ruff/src/rules" / dir_name(linter)
    rule_name_snake = snake_case(name)

    # Add the relevant `#testcase` macro.
    mod_rs = plugin_module / "mod.rs"
    content = mod_rs.read_text()

    with mod_rs.open("w") as fp:
        for line in content.splitlines():
            if line.strip() == "fn rules(rule_code: Rule, path: &Path) -> Result<()> {":
                indent = get_indent(line)
                fp.write(
                    f'{indent}#[test_case(Rule::{name}, Path::new("{code}.py"); '
                    f'"{code}")]',
                )
                fp.write("\n")

            fp.write(line)
            fp.write("\n")

    # Add the exports
    rules_dir = plugin_module / "rules"
    rules_mod = rules_dir / "mod.rs"

    contents = rules_mod.read_text()
    parts = contents.split("\n\n")
    if len(parts) == 2:
        new_contents = parts[0] + "\n"
        new_contents += f"pub use {rule_name_snake}::{{{rule_name_snake}, {name}}};"
        new_contents += "\n"
        new_contents += "\n"
        new_contents += parts[1]
        new_contents += f"mod {rule_name_snake};"
        new_contents += "\n"
        rules_mod.write_text(new_contents)
    else:
        with rules_mod.open("a") as fp:
            fp.write(f"pub use {rule_name_snake}::{{{rule_name_snake}, {name}}};")
            fp.write("\n")
            fp.write(f"mod {rule_name_snake};")
            fp.write("\n")

    # Add the relevant rule function.
    with (rules_dir / f"{rule_name_snake}.rs").open("w") as fp:
        fp.write(
            """use ruff_macros::{derive_message_formats, violation};
use ruff_diagnostics::Violation;

use crate::checkers::ast::Checker;

#[violation]
pub struct %s;
impl Violation for %s {
    #[derive_message_formats]
    fn message(&self) -> String {
        todo!("implement message");
        format!("TODO: write message")
    }
}
"""
            % (name, name),
        )
        fp.write("\n")
        fp.write(
            f"""
/// {code}
pub fn {rule_name_snake}(checker: &mut Checker) {{}}
""",
        )
        fp.write("\n")

    # Add the relevant code-to-violation pair to `src/registry.rs`.
    content = (ROOT_DIR / "crates/ruff/src/registry.rs").read_text()

    seen_macro = False
    has_written = False
    with (ROOT_DIR / "crates/ruff/src/registry.rs").open("w") as fp:
        for line in content.splitlines():
            fp.write(line)
            fp.write("\n")

            if has_written:
                continue

            if line.startswith("ruff_macros::register_rules!"):
                seen_macro = True
                continue

            if not seen_macro:
                continue

            if line.strip() == f"// {linter}":
                indent = get_indent(line)
                fp.write(f"{indent}rules::{dir_name(linter)}::rules::{name},")
                fp.write("\n")
                has_written = True

    assert has_written

    text = ""
    with (ROOT_DIR / "crates/ruff/src/codes.rs").open("r") as fp:
        while (line := next(fp)).strip() != f"// {linter}":
            text += line
        text += line
        linter_variant = pascal_case(linter)
        text += " " * 8 + f'({linter_variant}, "{code}") => Rule::{name},\n'
        text += fp.read()

    with (ROOT_DIR / "crates/ruff/src/codes.rs").open("w") as fp:
        fp.write(text)


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="Generate boilerplate for a new rule.",
        epilog=(
            "python scripts/add_rule.py "
            "--name PreferListBuiltin --code PIE807 --linter flake8-pie"
        ),
    )
    parser.add_argument(
        "--name",
        type=str,
        required=True,
        help="The name of the check to generate, in PascalCase (e.g., 'LineTooLong').",
    )
    parser.add_argument(
        "--code",
        type=str,
        required=True,
        help="The code of the check to generate (e.g., 'A001').",
    )
    parser.add_argument(
        "--linter",
        type=str,
        required=True,
        help="The source with which the check originated (e.g., 'flake8-builtins').",
    )
    args = parser.parse_args()

    main(name=args.name, code=args.code, linter=args.linter)
