#NOVA_CLI\nova_cli\cli\shell.py

import warnings
# Suppress the Deprecation/Future warnings from Google/Tiktoken cluttering the Mac terminal
warnings.filterwarnings("ignore", category=FutureWarning)
warnings.filterwarnings("ignore", category=UserWarning)

import os
import sys
from rich import print

if sys.platform == "win32":
    import pyreadline3 as readline
else:
    import readline

import shlex
import time
import logging
import re
from rich.panel import Panel

# ✅ Client-side API wrapper (CLI calls NOVA_API over HTTP)
from nova_cli.nova_core.ai.api_client import BridgeyeAPIClient

from nova_cli import config
import core.state as state
import core.prompts as prompts

# ✅ CLI-local registry/commands (no nova.* imports)
from nova_cli.cli.registry import CommandRegistry
from nova_cli.cli.commands import register_core_commands

# ✅ Client-side auth (CLI talks to nova-web / token endpoints)
from nova_cli.nova_core.auth.client import NovaAuthClient
from nova_cli.nova_core.auth.storage import save_auth

from nova_cli.local.ui import ui
from nova_cli.local.file_manager.path_ops import resolve_path
from nova_cli.local.file_manager.io_ops import load_file, map_directory, save_code_to_file
from nova_cli.local.file_manager.commands import handle_ai_commands
from nova_cli.local.file_manager.git_ops import (
    update_repo_path,
    manual_commit,
    perform_push,
    perform_pull,
    git_status,
)
from nova_cli.local.utils import extract_code_from_markdown
from nova_cli.local.healer.runner import run_with_healing


class NovaShell:
    def __init__(self):
        self.state = state.SessionState()
        self.interface = ui

        # API-only mode (no local model client)
        self.groq_client = None
        self.chat_session = None
        self.context_loader = None

        self.history_file = os.path.expanduser("~/.nova_history")
        try:
            if os.path.exists(self.history_file):
                readline.read_history_file(self.history_file)
        except Exception:
            pass

        # --- ROBUST TAB COMPLETION CONFIG ---
        def completer(text, state_idx):
            buffer = readline.get_line_buffer()
            line = buffer.lstrip()

            # If completing the first word, suggest commands
            if " " not in line:
                options = [cmd for cmd in self.registry.all().keys() if cmd.startswith(text)]
            else:
                # If completing arguments, suggest files/directories
                import glob
                matches = glob.glob(text + "*")
                options = []
                for m in matches:
                    if os.path.isdir(m):
                        options.append(m + "/")
                    else:
                        options.append(m)

            return options[state_idx] if state_idx < len(options) else None

        # Platform-safe readline setup (Windows / macOS / Linux)
        if hasattr(readline, "parse_and_bind"):
            doc = getattr(readline, "__doc__", "") or ""

            if sys.platform == "darwin" and "libedit" in doc:
                readline.parse_and_bind("bind ^I rl_complete")
            else:
                readline.parse_and_bind("tab: complete")
                readline.parse_and_bind("set show-all-if-ambiguous on")

            # Safe completer setup
            if hasattr(readline, "set_completer"):
                readline.set_completer(completer)

            # Safe delimiter handling
            if hasattr(readline, "get_completer_delims") and hasattr(readline, "set_completer_delims"):
                current_delims = readline.get_completer_delims()
                readline.set_completer_delims(current_delims.replace("/", ""))

        # Configuration
        self.provider = "groq"
        self.model_name = config.DEFAULT_MODEL
        self.project_root = os.path.abspath(os.getcwd())

        # Command Registry
        self.registry = CommandRegistry()
        register_core_commands(self.registry, self)

    def get_prompt_text(self):
        prefix = ""
        if self.state.active_file:
            prefix = f"[dim]({os.path.basename(self.state.active_file)})[/dim] "
        if len(self.state.loaded_files) > 0:
            prefix += f"[dim][{len(self.state.loaded_files)} loaded][/dim] "
        return f"{prefix}[bold cyan]spark terminal >[/bold cyan]  "

    # --- COMMAND HANDLERS ---

    def cmd_help(self, args):
        help_text = """
[bold cyan]NOVA COMMANDS[/bold cyan]
[green]Core Modes:[/green]
  run <cmd>    : Run a command with Auto-Healing (e.g., 'run python app.py')
  clean        : Run the Code Janitor to refactor/clean code

[green]File & Navigation:[/green]
  ls / :map    : Show file tree
  cd <path>    : Change directory
  pwd          : Print working directory
  :load <file> : Load a file into AI context
  :unload      : Unload files from context
  :paste       : Paste multiline text/logs

[green]System:[/green]
  :gitoptions  : Open Git operations menu
  :model       : Change AI Model
  :wizard      : Run Project Creation Wizard
  reset        : Reset session memory
  exit         : Shutdown
"""
        self.interface.print(Panel(help_text.strip(), title="Help Menu", border_style="cyan"))

    def cmd_gitoptions(self, args):
        while True:
            choice = self.interface.show_git_options(config.GIT_AUTO_COMMIT, config.GIT_AUTO_PUSH)

            if choice.startswith("Back"):
                break
            elif choice.startswith("Toggle Auto-Commit"):
                config.GIT_AUTO_COMMIT = not config.GIT_AUTO_COMMIT
                status = "ENABLED" if config.GIT_AUTO_COMMIT else "DISABLED"
                self.interface.print(f"[yellow]>> Auto-Commit is now {status}[/yellow]")
            elif choice.startswith("Toggle Auto-Push"):
                config.GIT_AUTO_PUSH = not config.GIT_AUTO_PUSH
                status = "ENABLED" if config.GIT_AUTO_PUSH else "DISABLED"
                self.interface.print(f"[yellow]>> Auto-Push is now {status}[/yellow]")
            elif choice.startswith("Manual Commit"):
                msg = self.interface.input("[cyan]Commit Message > [/cyan]").strip()
                if msg:
                    manual_commit(msg)
            elif choice.startswith("Push"):
                perform_push()
            elif choice.startswith("Pull"):
                perform_pull()
            elif choice.startswith("Git Status"):
                git_status()
                self.interface.input("[dim]Press Enter to continue...[/dim]")

    def cmd_login(self, args):
        auth = NovaAuthClient()

        print("[cyan]Opening browser for authentication...[/cyan]")
        session_id = auth.create_session()
        auth.open_browser(session_id)

        print("[dim]Waiting for approval...[/dim]")

        auth_code = auth.poll_session(session_id)

        if not auth_code:
            print("[red]Login timed out.[/red]")
            return

        tokens = auth.exchange_auth_code(auth_code)

        if not tokens or tokens.get("error"):
            print(f"[red]Authentication failed: {tokens}[/red]")
            return

        tokens["issued_at"] = time.time()
        save_auth(tokens)

        print("[green]Login successful! You are now authenticated.[/green]")

    def cmd_model(self, args):
        new_model, new_provider = self.interface.show_model_selector(self.model_name)
        if new_model != self.model_name:
            self.model_name = new_model
            self.provider = new_provider
            logging.info(f"Switched model to {self.model_name}")
            self.interface.display_header(self.model_name, os.getcwd())

    def cmd_reset(self, args):
        self.state.reset()
        self.interface.clear()
        logging.info("Session reset")
        self.interface.display_header(self.model_name, os.getcwd())

    def cmd_unload(self, args):
        if not args:
            self.state.active_file = None
            self.state.loaded_files = {}
            self.state.loaded_paths = {}
            self.interface.print("[dim]  Unloaded all files.[/dim]")
        else:
            fname = args.replace('"', "").replace("'", "").strip()
            found_key = None
            for key in self.state.loaded_files.keys():
                if key == fname or os.path.basename(key) == fname:
                    found_key = key
                    break

            if found_key:
                del self.state.loaded_files[found_key]
                if found_key in self.state.loaded_paths:
                    del self.state.loaded_paths[found_key]

                self.interface.print(f"[dim]  Unloaded: {found_key}[/dim]")

                if self.state.active_file and os.path.basename(self.state.active_file) == found_key:
                    self.state.active_file = None
            else:
                self.interface.print(f"[red]  File '{fname}' is not currently loaded.[/red]")


    def cmd_map(self, args):
        prompts.clear_file_tree_cache()
        map_directory()

    def cmd_wizard(self, args):
        self.interface.print("[yellow]Wizard is not available in API-only CLI mode yet.[/yellow]")

    def cmd_apply(self, args):
        save_code_to_file(self.state.active_file, self.state.last_generated_code)

    def cmd_cd(self, args):
        if not args:
            return
        try:
            target_path = os.path.abspath(args)
            if not target_path.startswith(self.project_root):
                self.interface.print("[bold red]SECURITY ALERT:[/bold red] Access denied outside project root.")
                logging.warning(f"Access denied: {target_path}")
                return

            os.chdir(target_path)
            self.interface.print(f"[dim]  cwd: {os.getcwd()}[/dim]")
            update_repo_path(os.getcwd())
        except Exception as e:
            self.interface.print(f"[red]{e}[/red]")

    def cmd_pwd(self, args):
        self.interface.print(f"[dim]{os.getcwd()}[/dim]")

    def cmd_build_it(self, args):
        plan_path = os.path.join(os.getcwd(), "PLAN.md")
        if not os.path.exists(plan_path):
            self.interface.print("[red]No PLAN.md found. Please describe your project first.[/red]")
            return

        with open(plan_path, "r", encoding="utf-8") as f:
            plan_content = f.read()

        # Command the model to be a Coder, not a Planner
        prompt = (
            "IMPLEMENTATION PHASE: Read the following PLAN.md and generate the FULL CODE for all files listed. "
            "Use [CREATE: path/to/file] tags followed by triple-backtick code blocks. "
            "Do NOT explain the code. Do NOT output more plans. Just implementation.\n\n"
            f"PLAN.md CONTENT:\n{plan_content}"
        )
        
        # Force Qwen for implementation
        self.handle_ai_request(prompt, override_model="qwen/qwen3-32b")

    def cmd_exit(self, args):
        try:
            readline.write_history_file(self.history_file)
        except Exception:
            pass
        logging.info("Shutdown")
        return "EXIT"

    def cmd_load(self, args):
        """
        :load should ONLY load file contents into context.
        It must NOT trigger any AI call.
        """
        try:
            path_arg = (args or "").strip().replace('"', "").replace("'", "")
            if not path_arg:
                self.interface.print("[yellow]Usage: :load <file>[/yellow]")
                return

            f_path, content = load_file(path_arg)
            if not f_path:
                return

            abs_path = os.path.abspath(f_path).replace("\\", "/")
            base = os.path.basename(f_path)

            # mark active (keep absolute path)
            self.state.active_file = abs_path

            # canonical storage: basename only
            self.state.loaded_files[base] = content
            self.state.loaded_paths[base] = abs_path

            self.interface.print(f"[green]>> Loaded into context:[/green] {base}")
            return  # IMPORTANT: do not return a string (prevents AI call)

        except Exception as e:
            self.interface.print(f"[red]Load failed: {e}[/red]")
            return



    def cmd_paste(self, args):
        if not self.state.active_file:
            self.interface.print("[red]Load a file first.[/red]")
            return
        error_log = self.interface.get_multiline_input()
        if error_log:
            return f"DEBUG_REQUEST: Fix {self.state.active_file}\nERROR:\n{error_log}"

    def cmd_run(self, args):
        if not args:
            self.interface.print("[yellow]Usage: run <filename> or run <command>[/yellow]")
            return

        target_file = args.strip().replace('"', "").replace("'", "")
        if target_file.lower().endswith((".html", ".htm")):
            import webbrowser
            fpath = os.path.abspath(target_file)
            if os.path.exists(fpath):
                self.interface.print(f"[green]>> Opening {os.path.basename(fpath)} in browser...[/green]")
                webbrowser.open(f"file://{fpath}")
                return
            else:
                self.interface.print(f"[red]>> File not found: {target_file}[/red]")
                return

        command_list = shlex.split(args)
        resolved = resolve_path(command_list[0]) if command_list else None

        if len(command_list) == 1 and resolved and os.path.exists(resolved):
            command_list[0] = resolved
            ext = os.path.splitext(resolved)[1].lower()

            runners = {
                ".py": [sys.executable],
                ".js": ["node"],
                ".ts": ["ts-node"],
                ".rb": ["ruby"],
                ".go": ["go", "run"],
                ".php": ["php"],
                ".sh": ["bash"],
                ".pl": ["perl"],
                ".lua": ["lua"],
            }

            if ext in runners:
                command_list = runners[ext] + command_list

        elif len(command_list) > 0 and command_list[0].endswith(".py"):
            if "python" not in command_list[0]:
                command_list.insert(0, sys.executable)

        try:
            output = run_with_healing(
                command_args=command_list,
                cwd=os.getcwd(),
                model=self.model_name,
                provider=self.provider,
                context=self.state.loaded_files,
                repo_map=None,
            )

            # runner prints stdout itself on success; print fallback output if any
            if output and output != "SUCCESS_SIGNAL":
                self.interface.console.print(output, markup=False)

        except Exception as e:
            self.interface.print(f"[bold red]Healer Error:[/bold red] {e}")

    def cmd_clean(self, args):
        if not self.state.loaded_files:
            self.interface.print("[yellow]No files loaded. Use :load first.[yellow]")
            return

        api = BridgeyeAPIClient()

        for filename, content in self.state.loaded_files.items():
            try:
                self.interface.print(f"[dim]Refactoring {filename}...[/dim]")

                resp = api.refactor(
                    filename=filename,
                    content=content,
                    model=self.model_name,
                    provider=self.provider,
                )

                file_path = (
                    self.state.loaded_paths.get(filename)
                    or (
                        self.state.active_file
                        if self.state.active_file and os.path.basename(self.state.active_file) == filename
                        else filename
                    )
                )

                # --- CASE 1: Janitor returned a Nova [EDIT] patch ---
                if isinstance(resp, str) and "[EDIT:" in resp:
                    # Quick client-side sanity check before applying
                    if "<<<<<<<" not in resp or "=======" not in resp or ">>>>>>>" not in resp:
                        self.interface.print(
                            f"[bold red]Invalid Janitor patch format for {filename} (missing SEARCH/REPLACE markers).[/bold red]"
                        )
                        self.interface.print((resp or "")[:2000])
                        continue

                    modified = handle_ai_commands(resp)

                    if not modified:
                        self.interface.print(
                            f"[bold red]Janitor returned an [EDIT] block but it could not be applied for {filename}.[/bold red]"
                        )
                        self.interface.print((resp or "")[:2000])
                        continue

                    # Reload file after successful patch
                    if os.path.exists(file_path):
                        with open(file_path, "r", encoding="utf-8") as f:
                            new_code = f.read()
                        self.state.loaded_files[filename] = new_code

                    self.interface.print(f"[green]✔ Refactored {filename}[/green]")
                    continue

                # --- CASE 2: Server returned plain refactored code (fallback path) ---
                # This should almost never happen now that Janitor is strict,
                # but we keep it as a safety fallback.
                if not isinstance(resp, str):
                    raise ValueError(f"Unexpected response type from Janitor: {type(resp)}")

                new_code = resp or ""

                with open(file_path, "w", encoding="utf-8") as f:
                    f.write(new_code)

                self.state.loaded_files[filename] = new_code
                self.interface.print(f"[green]✔ Refactored {filename}[/green]")

            except Exception as e:
                self.interface.print(f"[bold red]Janitor API error for {filename}:[/bold red] {e}")


        def scan_and_load_context(self, text):
            potential_files = re.findall(r"\b[\w\-\/]+\.\w+\b", text)

            loaded_any = False
            for fname in potential_files:
                if os.path.exists(fname) and os.path.isfile(fname):
                    try:
                        abs_path = os.path.abspath(fname).replace("\\", "/")
                        base = os.path.basename(fname)

                        with open(fname, "r", encoding="utf-8") as f:
                            self.state.loaded_files[base] = f.read()
                        self.state.loaded_paths[base] = abs_path

                        if not self.state.active_file:
                            self.state.active_file = abs_path

                        loaded_any = True
                    except Exception:
                        pass

            if self.state.active_file and os.path.exists(self.state.active_file):
                try:
                    base = os.path.basename(self.state.active_file)
                    with open(self.state.active_file, "r", encoding="utf-8") as f:
                        self.state.loaded_files[base] = f.read()
                    self.state.loaded_paths[base] = os.path.abspath(self.state.active_file).replace("\\", "/")
                except Exception:
                    pass

            if loaded_any:
                self.interface.print("[dim]>> Auto-loaded file context for AI visibility.[/dim]")


    def run(self):
        self.interface.clear()
        logging.info(f"Session started in {self.project_root}")

        # Simple non-interactive entry
        if len(sys.argv) > 1:
            cmd = sys.argv[1]
            args = sys.argv[2:] if len(sys.argv) > 2 else []

            if cmd == "run":
                if args:
                    try:
                        output = run_with_healing(
                            command_args=args,
                            cwd=os.getcwd(),
                            model=self.model_name,
                            provider=self.provider,
                            context=self.state.loaded_files,
                            repo_map=prompts.get_repo_map_cached(os.getcwd()),
                        )
                        if output and output != "SUCCESS_SIGNAL":
                            print(output)
                    except Exception as e:
                        print(f"Healer Error: {e}")
                    return
                print("Usage: nova run <command>")
                return

            if cmd == "clean":
                # in non-interactive mode, clean requires loaded context; keep it simple for now
                print("Use interactive mode for clean (load files first).")
                return

            if cmd.lower() == "build":
                self.cmd_build_it(args)
                return

        self.interface.display_header(self.model_name, os.getcwd())

        while True:
            try:
                cmd_raw = self.interface.input(self.get_prompt_text()).strip()
                if not cmd_raw:
                    continue

                try:
                    parts = shlex.split(cmd_raw)
                except ValueError:
                    self.interface.print("[red]Syntax Error: Unbalanced quotes[/red]")
                    continue

                cmd_base = parts[0]
                args = " ".join(parts[1:]) if len(parts) > 1 else ""

                ai_prompt = None

                handler = self.registry.get(cmd_base)

                if handler:
                    result = handler(args)
                    if result == "EXIT":
                        break
                    if isinstance(result, str):
                        ai_prompt = result
                else:
                    ai_prompt = cmd_raw

                if ai_prompt:
                    self.handle_ai_request(ai_prompt)

            except KeyboardInterrupt:
                self.interface.print("\n[dim]Shutdown.[/dim]")
                break
            except EOFError:
                try:
                    readline.write_history_file(self.history_file)
                except Exception:
                    pass
                self.interface.print("\n[dim]Shutdown.[/dim]")
                break

    def handle_ai_request(self, prompt_text, override_model=None):
        try:
            api = BridgeyeAPIClient()

            # --- INTENT DETECTION ---
            prompt_lower = prompt_text.lower()
            # Expanded triggers to catch "do this project" or "start with this"
            build_keywords = ["build", "make", "create", "implement", "do it", "do this", "start", "proceed"]
            plan_keywords = ["plan", "outline", "todo", "to-do"]

            is_build_intent = any(k in prompt_lower for k in build_keywords)
            has_plan_word = any(k in prompt_lower for k in plan_keywords)
            
            plan_path = os.path.join(os.getcwd(), "PLAN.md")
            
            # Switch to Qwen only if build intent is clear and we aren't discussing the "plan"
            if is_build_intent and not has_plan_word and os.path.exists(plan_path) and not override_model:
                self.interface.print("[cyan]>> Intent detected: Switching to Qwen-3 to build from PLAN.md...[/cyan]")
                return self.cmd_build_it(prompt_text)
            # ------------------------

            target_model = override_model if override_model else self.model_name
            
            if not override_model:
                prompt_text = (
                    "INSTRUCTION: You are in the PLANNING PHASE.\n"
                    "1. If the request is a new project or task, create a detailed To-Do list in PLAN.md using [CREATE: PLAN.md].\n"
                    "2. If the user is giving feedback (e.g. 'cool', 'looks good') or asking a question, respond conversationally "
                    "and do NOT recreate the PLAN.md file.\n"
                    "3. NEVER write the actual implementation code in this phase.\n\n"
                    f"USER REQUEST: {prompt_text}"
                )

            with self.interface.create_loader(""):
                output = api.chat(
                    prompt=prompt_text,
                    context=self.state.loaded_files,
                    model=target_model,
                    provider=self.provider,
                )

            if not output:
                return

            self.interface.print(output)
            self.state.last_ai_response = output

            extracted_code = extract_code_from_markdown(output)
            if extracted_code:
                self.state.last_generated_code = extracted_code

            created_files = handle_ai_commands(output)

            if isinstance(created_files, list) and created_files:
                last_file = created_files[-1]
                if os.path.exists(last_file):
                    self.state.active_file = os.path.abspath(last_file)
                    with open(last_file, "r", encoding="utf-8") as f:
                        self.state.loaded_files[os.path.basename(last_file)] = f.read()

            logging.info(f"Interaction | Len: {len(output)}")

        except Exception as e:
            self.interface.print(f"[bold red]API Error:[/bold red] {e}")
            logging.error(f"Chat API Error: {e}")
