"""
NLS CLI - Command-line interface for nlsc

Commands:
    nlsc init              Initialize a new NLS project
    nlsc compile <file>    Compile .nl file to target language
    nlsc verify <file>     Verify .nl file without generating
    nlsc graph <file>      Visualize dependencies and dataflow
    nlsc test <file>       Run @test specifications
    nlsc atomize <file>    Extract ANLUs from Python code
    nlsc diff <file>       Show changes since last compile
    nlsc watch <dir>       Watch directory for .nl changes
"""

import argparse
import subprocess
import sys
import tempfile
from pathlib import Path

from . import __version__
from .parser import parse_nl_path, ParseError
from .schema import NLFile
from .resolver import resolve_dependencies
from .emitter import emit_python, emit_tests
from .lockfile import generate_lockfile, write_lockfile, verify_lockfile
from .graph import (
    emit_mermaid,
    emit_dot,
    emit_ascii,
    emit_dataflow_mermaid,
    emit_dataflow_ascii,
    emit_fsm_mermaid,
)
from .atomize import atomize_file
from .diff import get_anlu_changes, format_changes_output, format_stat_output, generate_full_diff
from .lockfile import read_lockfile
from .watch import NLWatcher, format_timestamp

# Unicode symbols with ASCII fallbacks for Windows console
def _check() -> str:
    """Return checkmark, falling back to ASCII if needed."""
    try:
        "\u2713".encode(sys.stdout.encoding or 'utf-8')
        return "\u2713"
    except (UnicodeEncodeError, LookupError):
        return "[OK]"

def _cross() -> str:
    """Return cross mark, falling back to ASCII if needed."""
    try:
        "\u2717".encode(sys.stdout.encoding or 'utf-8')
        return "\u2717"
    except (UnicodeEncodeError, LookupError):
        return "[X]"

# Parser selection
_use_treesitter = False


def set_parser_backend(backend: str) -> None:
    """Set the parser backend to use: 'regex' or 'treesitter'."""
    global _use_treesitter
    if backend == "treesitter":
        # Check if tree-sitter is available
        try:
            from . import parser_treesitter
            if not parser_treesitter.is_available():
                raise ImportError("tree-sitter is not installed")
        except ImportError as e:
            print(f"Error: Tree-sitter parser not available: {e}", file=sys.stderr)
            print("Install with: pip install nlsc[treesitter]", file=sys.stderr)
            sys.exit(1)
        _use_treesitter = True
    else:
        _use_treesitter = False


def parse_nl_file_auto(source_path: Path) -> NLFile:
    """Parse a .nl file using the selected parser backend."""
    if _use_treesitter:
        from .parser_treesitter import parse_nl_path_treesitter
        return parse_nl_path_treesitter(source_path)
    else:
        return parse_nl_path(source_path)


def cmd_init(args: argparse.Namespace) -> int:
    """Initialize a new NLS project"""
    project_dir = Path(args.path or ".")

    # Create project directory if it doesn't exist
    project_dir.mkdir(parents=True, exist_ok=True)

    print(f"Initializing NLS project in {project_dir.absolute()}...")

    # Create config file
    config_path = project_dir / "nl.config.yaml"
    if not config_path.exists():
        config_content = """\
# NLS Project Configuration
# Generated by nlsc init

project:
  name: my-project
  version: 0.1.0

compiler:
  default_target: python
  llm_backend: mock  # mock, claude, openai, ollama
  cache_strategy: aggressive

targets:
  python:
    version: ">=3.11"
    style: black
    type_hints: strict

validation:
  require_purpose: true
  require_guards: false
  max_anlu_complexity: 10
"""
        config_path.write_text(config_content, encoding="utf-8")
        print(f"  {_check()} Created {config_path.name}")
    else:
        print(f"  • {config_path.name} already exists")

    # Create directories
    src_dir = project_dir / "src"
    tests_dir = project_dir / "tests"

    for dir_path in [src_dir, tests_dir]:
        if not dir_path.exists():
            dir_path.mkdir(parents=True)
            print(f"  {_check()} Created {dir_path.name}/")
        else:
            print(f"  • {dir_path.name}/ already exists")

    # Create __init__.py files
    for init_path in [src_dir / "__init__.py", tests_dir / "__init__.py"]:
        if not init_path.exists():
            init_path.write_text("", encoding="utf-8")

    print("\nNLS project initialized! Next steps:")
    print("  1. Create a .nl file in src/")
    print("  2. Run: nlsc compile src/your-file.nl")

    return 0


def cmd_compile(args: argparse.Namespace) -> int:
    """Compile a .nl file to target language"""
    source_path = Path(args.file)

    if not source_path.exists():
        print(f"Error: File not found: {source_path}", file=sys.stderr)
        return 1

    parser_name = "tree-sitter" if _use_treesitter else "regex"
    print(f"Compiling {source_path} (parser: {parser_name})...")

    # Parse
    try:
        nl_file = parse_nl_file_auto(source_path)
        print(f"  {_check()} Parsed {len(nl_file.anlus)} ANLUs")
    except ParseError as e:
        print(f"  {_cross()} Parse error: {e}", file=sys.stderr)
        return 1

    # Resolve dependencies
    result = resolve_dependencies(nl_file)
    if not result.success:
        print(f"  {_cross()} Resolution errors:", file=sys.stderr)
        for err in result.errors:
            print(f"    - {err.anlu_id}: {err.message}", file=sys.stderr)
        return 1
    print(f"  {_check()} Resolved dependencies")

    # Emit Python
    target = args.target or "python"
    if target != "python":
        print(f"  {_cross()} Target '{target}' not yet supported", file=sys.stderr)
        return 1

    python_code = emit_python(nl_file, mode="mock")

    # Determine output path
    output_path = source_path.with_suffix(".py")
    if args.output:
        output_path = Path(args.output)

    output_path.write_text(python_code, encoding="utf-8")
    line_count = python_code.count("\n") + 1
    print(f"  {_check()} Generated {output_path.name} ({line_count} lines)")

    # Generate tests if present
    test_code = emit_tests(nl_file)
    if test_code and nl_file.tests:
        test_path = source_path.parent / f"test_{source_path.stem}.py"
        test_path.write_text(test_code, encoding="utf-8")
        print(f"  {_check()} Generated {test_path.name}")

    # Generate lockfile
    lock_path = source_path.with_suffix(".nl.lock")
    lockfile = generate_lockfile(
        nl_file,
        python_code,
        str(output_path),
        llm_backend="mock"
    )
    write_lockfile(lockfile, lock_path)
    print(f"  {_check()} Updated {lock_path.name}")

    print("\nCompilation complete!")
    return 0


def cmd_verify(args: argparse.Namespace) -> int:
    """Verify a .nl file without generating code"""
    source_path = Path(args.file)

    if not source_path.exists():
        print(f"Error: File not found: {source_path}", file=sys.stderr)
        return 1

    parser_name = "tree-sitter" if _use_treesitter else "regex"
    print(f"Verifying {source_path} (parser: {parser_name})...")

    # Parse
    try:
        nl_file = parse_nl_file_auto(source_path)
        print(f"  {_check()} Syntax valid: {len(nl_file.anlus)} ANLUs")
    except ParseError as e:
        print(f"  {_cross()} Parse error: {e}", file=sys.stderr)
        return 1

    # Resolve
    result = resolve_dependencies(nl_file)
    if not result.success:
        print(f"  {_cross()} Resolution errors:")
        for err in result.errors:
            print(f"    - {err.anlu_id}: {err.message}")
        return 1
    print(f"  {_check()} Dependencies valid")

    # Validate each ANLU
    errors = []
    for anlu in nl_file.anlus:
        if not anlu.purpose:
            errors.append(f"{anlu.identifier}: Missing PURPOSE")
        if not anlu.returns:
            errors.append(f"{anlu.identifier}: Missing RETURNS")

    if errors:
        print(f"  {_cross()} Validation errors:")
        for err in errors:
            print(f"    - {err}")
        return 1

    print(f"  {_check()} All ANLUs valid")
    print("\nVerification passed!")
    return 0


def cmd_graph(args: argparse.Namespace) -> int:
    """Generate dependency graph visualization"""
    source_path = Path(args.file)

    if not source_path.exists():
        print(f"Error: File not found: {source_path}", file=sys.stderr)
        return 1

    # Parse
    try:
        nl_file = parse_nl_file_auto(source_path)
    except ParseError as e:
        print(f"Parse error: {e}", file=sys.stderr)
        return 1

    output_format = args.format or "mermaid"
    anlu_id = args.anlu
    dataflow = args.dataflow

    # If specific ANLU requested
    if anlu_id:
        anlu = nl_file.get_anlu(anlu_id)
        if not anlu:
            print(f"Error: ANLU '{anlu_id}' not found", file=sys.stderr)
            print(f"Available: {', '.join(a.identifier for a in nl_file.anlus)}")
            return 1

        # Check if ANLU has FSM states
        has_fsm = bool(anlu.fsm_states())

        if output_format == "mermaid":
            if has_fsm and not dataflow:
                output = emit_fsm_mermaid(anlu)
            else:
                output = emit_dataflow_mermaid(anlu)
        elif output_format == "ascii":
            output = emit_dataflow_ascii(anlu)
        else:
            print(f"Error: Format '{output_format}' not supported for dataflow", file=sys.stderr)
            return 1
    else:
        # Full file dependency graph
        if output_format == "mermaid":
            output = emit_mermaid(nl_file)
        elif output_format == "dot":
            output = emit_dot(nl_file)
        elif output_format == "ascii":
            output = emit_ascii(nl_file)
        else:
            print(f"Error: Unknown format '{output_format}'", file=sys.stderr)
            return 1

    # Output
    if args.output:
        output_path = Path(args.output)
        output_path.write_text(output, encoding="utf-8")
        print(f"Graph written to {output_path}")
    else:
        print(output)

    return 0


def cmd_test(args: argparse.Namespace) -> int:
    """Run @test specifications from a .nl file"""
    source_path = Path(args.file)

    if not source_path.exists():
        print(f"Error: File not found: {source_path}", file=sys.stderr)
        return 1

    # Parse
    try:
        nl_file = parse_nl_file_auto(source_path)
    except ParseError as e:
        print(f"Parse error: {e}", file=sys.stderr)
        return 1

    # Check for tests
    if not nl_file.tests:
        print(f"No @test blocks found in {source_path}")
        return 0

    # Create temp directory for generated code
    with tempfile.TemporaryDirectory() as temp_dir:
        temp_path = Path(temp_dir)

        # Generate the module code
        python_code = emit_python(nl_file, mode="mock")
        module_name = nl_file.module.name.replace("-", "_")
        module_path = temp_path / f"{module_name}.py"
        module_path.write_text(python_code, encoding="utf-8")

        # Generate the test code
        test_code = emit_tests(nl_file)
        if not test_code:
            print(f"No tests generated from {source_path}")
            return 0

        # Fix import to be direct (not relative)
        test_code = test_code.replace(f"from .{module_name} import *", f"from {module_name} import *")
        test_path = temp_path / f"test_{module_name}.py"
        test_path.write_text(test_code, encoding="utf-8")

        # Create __init__.py
        init_path = temp_path / "__init__.py"
        init_path.write_text("", encoding="utf-8")

        # Print summary
        total_cases = sum(len(ts.cases) for ts in nl_file.tests)
        print(f"Running {total_cases} test cases from {source_path}...")
        for ts in nl_file.tests:
            print(f"  • [{ts.anlu_id}]: {len(ts.cases)} cases")
        print()

        # Run pytest
        verbose_flag = "-v" if getattr(args, "verbose", False) else "-q"
        result = subprocess.run(
            [sys.executable, "-m", "pytest", str(test_path), verbose_flag, "--tb=short"],
            cwd=str(temp_path),
            capture_output=not getattr(args, "verbose", False),
            text=True,
            env={**__import__("os").environ, "PYTHONPATH": str(temp_path)}
        )

        # Report results
        if result.returncode == 0:
            print(f"{_check()} All {total_cases} tests passed!")
            return 0
        else:
            print(f"{_cross()} Tests failed")
            if result.stdout:
                print(result.stdout)
            if result.stderr:
                print(result.stderr, file=sys.stderr)
            return 1


def cmd_atomize(args: argparse.Namespace) -> int:
    """Extract ANLUs from Python source code"""
    source_path = Path(args.file)

    if not source_path.exists():
        print(f"Error: File not found: {source_path}", file=sys.stderr)
        return 1

    # Determine output path
    output_path = Path(args.output) if args.output else None

    # Determine module name
    module_name = args.module

    print(f"Atomizing {source_path}...")

    try:
        nl_content = atomize_file(source_path, output_path, module_name)

        # Count ANLUs
        anlu_count = nl_content.count("[") - nl_content.count("[[")
        final_output = output_path or source_path.with_suffix(".nl")

        print(f"  {_check()} Extracted {anlu_count} ANLUs")
        print(f"  {_check()} Wrote {final_output}")
        return 0
    except SyntaxError as e:
        print(f"  {_cross()} Python syntax error: {e}", file=sys.stderr)
        return 1
    except Exception as e:
        print(f"  {_cross()} Error: {e}", file=sys.stderr)
        return 1


def cmd_diff(args: argparse.Namespace) -> int:
    """Show changes since last compile"""
    source_path = Path(args.file)

    if not source_path.exists():
        print(f"Error: File not found: {source_path}", file=sys.stderr)
        return 1

    # Parse current NL file
    try:
        nl_file = parse_nl_file_auto(source_path)
    except ParseError as e:
        print(f"Parse error: {e}", file=sys.stderr)
        return 1

    # Load lockfile if exists
    lock_path = source_path.with_suffix(".nl.lock")
    lockfile = None
    if lock_path.exists():
        try:
            lockfile = read_lockfile(lock_path)
        except Exception as e:
            print(f"Warning: Could not read lockfile: {e}", file=sys.stderr)

    # Get changes
    changes = get_anlu_changes(nl_file, lockfile)

    if lockfile is None:
        print("No lockfile found. All ANLUs shown as new.\n")

    # Output based on flags
    if args.stat:
        print(format_stat_output(changes))
    elif args.full:
        # Generate Python code for diff
        from .emitter import emit_python

        py_code_new = emit_python(nl_file)

        # Get original Python code from lockfile target
        py_path = source_path.with_suffix(".py")
        if py_path.exists():
            py_code_orig = py_path.read_text(encoding="utf-8")
            print(generate_full_diff(py_code_orig, py_code_new, py_path.name))
        else:
            print("No existing Python file to diff against.")
            print(format_changes_output(changes))
    else:
        print(format_changes_output(changes))

    return 0


def cmd_watch(args: argparse.Namespace) -> int:
    """Watch directory for .nl file changes and recompile"""
    watch_path = Path(args.dir)

    if not watch_path.exists():
        print(f"Error: Directory not found: {watch_path}", file=sys.stderr)
        return 1

    if not watch_path.is_dir():
        print(f"Error: Not a directory: {watch_path}", file=sys.stderr)
        return 1

    quiet = args.quiet
    run_tests = args.test
    debounce_ms = args.debounce

    def on_compile(path: Path, success: bool, error: str | None) -> None:
        """Callback for compile events"""
        timestamp = format_timestamp()
        if success:
            if not quiet:
                print(f"{timestamp} " + _check() + "  Compiled {path.name}")
        else:
            print(f"{timestamp} " + _cross() + "  {path.name}: {error}", file=sys.stderr)

    print(f"Watching {watch_path.absolute()} for .nl changes...")
    print("Press Ctrl+C to stop.\n")

    watcher = NLWatcher(
        watch_path=watch_path,
        debounce_ms=debounce_ms,
        quiet=quiet,
        run_tests=run_tests,
        on_compile=on_compile,
    )

    try:
        watcher.start()
    except KeyboardInterrupt:
        watcher.stop()
        print("\nStopped watching.")

    return 0


def cmd_lock_check(args: argparse.Namespace) -> int:
    """Verify that lockfile is current with source"""
    source_path = Path(args.file)

    if not source_path.exists():
        print(f"Error: File not found: {source_path}", file=sys.stderr)
        return 1

    lock_path = source_path.with_suffix(".nl.lock")
    if not lock_path.exists():
        print(f"Error: Lockfile not found: {lock_path}", file=sys.stderr)
        return 1

    # Parse current NL file
    try:
        nl_file = parse_nl_file_auto(source_path)
    except ParseError as e:
        print(f"Parse error: {e}", file=sys.stderr)
        return 1

    # Read lockfile
    lockfile = read_lockfile(lock_path)
    if not lockfile:
        print(f"Error: Could not read lockfile: {lock_path}", file=sys.stderr)
        return 1

    # Verify
    errors = verify_lockfile(lockfile, nl_file)

    if errors:
        print("Lockfile out of date:")
        for err in errors:
            print(f"  • {err}")
        return 1

    print(f"{_check()} Lockfile is current")
    return 0


def cmd_lock_update(args: argparse.Namespace) -> int:
    """Regenerate lockfile from current source and compiled output"""
    source_path = Path(args.file)

    if not source_path.exists():
        print(f"Error: File not found: {source_path}", file=sys.stderr)
        return 1

    # Parse current NL file
    try:
        nl_file = parse_nl_file_auto(source_path)
    except ParseError as e:
        print(f"Parse error: {e}", file=sys.stderr)
        return 1

    # Find compiled Python file
    py_path = source_path.with_suffix(".py")
    if not py_path.exists():
        print(f"Warning: Compiled file not found, generating fresh: {py_path}")
        python_code = emit_python(nl_file, mode="mock")
    else:
        python_code = py_path.read_text(encoding="utf-8")

    # Generate lockfile
    lock_path = source_path.with_suffix(".nl.lock")
    lockfile = generate_lockfile(
        nl_file,
        python_code,
        str(py_path),
        llm_backend="mock"
    )
    write_lockfile(lockfile, lock_path)

    print(f"{_check()} Updated {lock_path.name}")
    return 0


def main() -> int:
    """Main entry point for nlsc CLI"""
    parser = argparse.ArgumentParser(
        prog="nlsc",
        description="Natural Language Source Compiler",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""\
Examples:
  nlsc init                     Initialize new NLS project
  nlsc compile src/math.nl      Compile to Python
  nlsc verify src/auth.nl       Validate without generating
  nlsc graph src/order.nl       Generate Mermaid dependency diagram
  nlsc graph src/order.nl -a process-order  Visualize ANLU dataflow

The conversation is the programming. The .nl file is the receipt.
"""
    )
    parser.add_argument(
        "--version",
        action="version",
        version=f"nlsc {__version__}"
    )
    parser.add_argument(
        "--parser",
        choices=["regex", "treesitter"],
        default="regex",
        help="Parser backend: 'regex' (default) or 'treesitter'"
    )

    subparsers = parser.add_subparsers(dest="command", help="Available commands")

    # init command
    init_parser = subparsers.add_parser("init", help="Initialize NLS project")
    init_parser.add_argument(
        "path",
        nargs="?",
        default=".",
        help="Project directory (default: current)"
    )

    # compile command
    compile_parser = subparsers.add_parser("compile", help="Compile .nl file")
    compile_parser.add_argument(
        "file",
        help="Path to .nl file"
    )
    compile_parser.add_argument(
        "-t", "--target",
        default="python",
        help="Target language (default: python)"
    )
    compile_parser.add_argument(
        "-o", "--output",
        help="Output file path"
    )

    # verify command
    verify_parser = subparsers.add_parser("verify", help="Verify .nl file")
    verify_parser.add_argument(
        "file",
        help="Path to .nl file"
    )

    # graph command
    graph_parser = subparsers.add_parser(
        "graph",
        help="Visualize dependencies and dataflow"
    )
    graph_parser.add_argument(
        "file",
        help="Path to .nl file"
    )
    graph_parser.add_argument(
        "-f", "--format",
        choices=["mermaid", "dot", "ascii"],
        default="mermaid",
        help="Output format (default: mermaid)"
    )
    graph_parser.add_argument(
        "-a", "--anlu",
        help="Specific ANLU for dataflow visualization"
    )
    graph_parser.add_argument(
        "--dataflow",
        action="store_true",
        help="Show dataflow instead of FSM states"
    )
    graph_parser.add_argument(
        "-o", "--output",
        help="Output file path (default: stdout)"
    )

    # test command
    test_parser = subparsers.add_parser(
        "test",
        help="Run @test specifications"
    )
    test_parser.add_argument(
        "file",
        help="Path to .nl file"
    )
    test_parser.add_argument(
        "-v", "--verbose",
        action="store_true",
        help="Verbose output"
    )

    # atomize command
    atomize_parser = subparsers.add_parser(
        "atomize",
        help="Extract ANLUs from Python code"
    )
    atomize_parser.add_argument(
        "file",
        help="Path to Python file"
    )
    atomize_parser.add_argument(
        "-o", "--output",
        help="Output .nl file path"
    )
    atomize_parser.add_argument(
        "-m", "--module",
        help="Module name for generated .nl"
    )

    # diff command
    diff_parser = subparsers.add_parser(
        "dif",
        help="Show changes since last compile"
    )
    diff_parser.add_argument(
        "file",
        help="Path to .nl file"
    )
    diff_parser.add_argument(
        "--stat",
        action="store_true",
        help="Show summary only"
    )
    diff_parser.add_argument(
        "--full",
        action="store_true",
        help="Show full unified dif"
    )

    # watch command
    watch_parser = subparsers.add_parser(
        "watch",
        help="Watch directory for .nl changes"
    )
    watch_parser.add_argument(
        "dir",
        nargs="?",
        default=".",
        help="Directory to watch (default: current)"
    )
    watch_parser.add_argument(
        "-q", "--quiet",
        action="store_true",
        help="Suppress success messages"
    )
    watch_parser.add_argument(
        "-t", "--test",
        action="store_true",
        help="Run tests after successful compile"
    )
    watch_parser.add_argument(
        "-d", "--debounce",
        type=int,
        default=100,
        help="Debounce interval in ms (default: 100)"
    )

    # lock:check command
    lock_check_parser = subparsers.add_parser(
        "lock:check",
        help="Verify lockfile is current"
    )
    lock_check_parser.add_argument(
        "file",
        help="Path to .nl file"
    )

    # lock:update command
    lock_update_parser = subparsers.add_parser(
        "lock:update",
        help="Regenerate lockfile"
    )
    lock_update_parser.add_argument(
        "file",
        help="Path to .nl file"
    )

    args = parser.parse_args()

    if args.command is None:
        parser.print_help()
        return 0

    # Set parser backend before any parsing commands
    if hasattr(args, "parser") and args.parser:
        set_parser_backend(args.parser)

    if args.command == "init":
        return cmd_init(args)
    elif args.command == "compile":
        return cmd_compile(args)
    elif args.command == "verify":
        return cmd_verify(args)
    elif args.command == "graph":
        return cmd_graph(args)
    elif args.command == "test":
        return cmd_test(args)
    elif args.command == "atomize":
        return cmd_atomize(args)
    elif args.command == "dif":
        return cmd_diff(args)
    elif args.command == "watch":
        return cmd_watch(args)
    elif args.command == "lock:check":
        return cmd_lock_check(args)
    elif args.command == "lock:update":
        return cmd_lock_update(args)

    return 0


if __name__ == "__main__":
    sys.exit(main())
