#!/usr/bin/env python3
#
#    corindo-cli, a simple REPL AI agent.
#    Copyright (C) 2026 Ying-Chun Liu (PaulLiu) <paulliu@debian.org>
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <https://www.gnu.org/licenses/>.

import json
import os
import pathlib
import readline
import subprocess
import sys
import tomllib
from openai import OpenAI


def load_config():
  """Loads configuration from ~/.config/corindo-cli/config.toml,

  with fallback to legacy ~/.debgpt/config.toml.
  """
  config_path = pathlib.Path.home() / ".config" / "corindo-cli" / "config.toml"

  if not config_path.exists():
    # Fallback to legacy path
    legacy_path = pathlib.Path.home() / ".debgpt" / "config.toml"
    if legacy_path.exists():
      config_path = legacy_path
    else:
      print(f"❌ Configuration file not found:")
      print(f"   - Tried: ~/.config/corindo-cli/config.toml")
      print(f"   - Tried: ~/.debgpt/config.toml")
      print(
          "Please create one of them and fill in 'openai_api_key',"
          " 'openai_base_url', and 'openai_model'."
      )
      sys.exit(1)

  try:
    with open(config_path, "rb") as f:
      return tomllib.load(f), config_path
  except Exception as e:
    print(f"❌ Failed to parse TOML config file ({config_path}): {e}")
    sys.exit(1)


# Load configuration and initialize the OpenAI client
config, loaded_path = load_config()
API_KEY = config.get("openai_api_key")
BASE_URL = config.get("openai_base_url")
MODEL_NAME = config.get("openai_model", "gpt-4o")

if not API_KEY:
  print("❌ Configuration error: Missing required field 'openai_api_key'")
  sys.exit(1)

client_kwargs = {"api_key": API_KEY}
if BASE_URL:
  client_kwargs["base_url"] = BASE_URL

client = OpenAI(**client_kwargs)


# Strict user confirmation helper (requires explicit Y or N)
def get_user_confirmation(prompt_text: str) -> bool:
  while True:
    try:
      user_input = input(prompt_text).strip().upper()
      if user_input == "Y":
        return True
      elif user_input == "N":
        return False
      else:
        print(
            "⚠️ Invalid input: Please enter 'Y' or 'N' (case-insensitive)."
        )
    except (KeyboardInterrupt, EOFError):
      print("\nOperation cancelled by user.")
      return False


# Smart truncation helper to protect the model context window
def truncate_output(output: str, max_chars: int = 8000) -> str:
  """Truncates output by preserving the head and tail if it exceeds max_chars."""
  if len(output) <= max_chars:
    return output

  half = max_chars // 2
  head = output[:half]
  tail = output[-half:]

  return (
      f"{head}\n\n... [Output truncated: omitted approx"
      f" {len(output) - max_chars} characters in between] ...\n\n{tail}"
  )


# Define function calling tools
tools = [
    {
        "type": "function",
        "function": {
            "name": "run_shell_command",
            "description": (
                "Executes a shell command safely, such as debuild, git status,"
                " cargo build, etc."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "command": {
                        "type": "string",
                        "description": "The bash command to execute",
                    }
                },
                "required": ["command"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "read_file",
            "description": (
                "Reads content of a specified file path. Optionally accepts start_line and end_line (1-indexed) to read a specific range."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "filepath": {
                        "type": "string",
                        "description": "File path to read",
                    },
                    "start_line": {
                        "type": "integer",
                        "description": "Optional 1-indexed starting line number (inclusive)",
                    },
                    "end_line": {
                        "type": "integer",
                        "description": "Optional 1-indexed ending line number (inclusive)",
                    },
                },
                "required": ["filepath"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "write_file",
            "description": (
                "Writes or overwrites content into a specified file path"
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "filepath": {
                        "type": "string",
                        "description": "File path to write",
                    },
                    "content": {
                        "type": "string",
                        "description": "Content to write",
                    },
                },
                "required": ["filepath", "content"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "fetch_webpage",
            "description": (
                "Fetches and extracts the text content of a public webpage given a URL."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "url": {
                        "type": "string",
                        "description": "The public URL to fetch",
                    }
                },
                "required": ["url"],
            },
        },
    },
]


# Tool execution logic
def execute_tool(name: str, arguments: dict) -> str:
  if name == "run_shell_command":
    cmd = arguments.get("command", "")
    if any(
        danger in cmd
        for danger in ["rm -rf /", "mkfs", "> /dev/sd", ":(){ :|:& };:"]
    ):
      return "Error: Command blocked due to critical safety risk."

    prompt = (
        f"\n⚠️ [Security Confirmation] Agent requests to execute shell"
        f" command:\n  > {cmd}\nAllow execution? (Y/N): "
    )
    if not get_user_confirmation(prompt):
      return "Execution cancelled: User explicitly denied command execution."

    print(f"\n🚀 Executing: {cmd}\n" + "-" * 40)
    try:
      # Popen for real-time streaming, no timeout to support long builds
      # (e.g. debuild)
      process = subprocess.Popen(
          cmd,
          shell=True,
          stdout=subprocess.PIPE,
          stderr=subprocess.STDOUT,
          text=True,
      )

      output_lines = []
      for line in process.stdout:
        print(line, end="")
        output_lines.append(line)

      process.wait()
      print("-" * 40)

      full_output = "".join(output_lines)
      if not full_output.strip():
        return "Command executed successfully with no output."

      # Smart truncation before sending back to LLM context
      return truncate_output(full_output, max_chars=8000)

    except KeyboardInterrupt:
      if process.poll() is None:
        process.terminate()
      print("\n⚠️ Command terminated manually by user (Ctrl + C).")
      return "Execution cancelled by user during runtime."
    except Exception as e:
      return f"Execution error: {str(e)}"

  elif name == "write_file":
    filepath = arguments.get("filepath", "")
    content = arguments.get("content", "")

    prompt = (
        f"\n⚠️ [Security Confirmation] Agent requests to write/modify"
        f" file:\n  > {filepath}\nAllow file write? (Y/N): "
    )
    if not get_user_confirmation(prompt):
      return "Execution cancelled: User explicitly denied file write."

    try:
      with open(filepath, "w", encoding="utf-8") as f:
        f.write(content)
      return f"Successfully wrote content to {filepath}"
    except Exception as e:
      return f"Error writing to file: {str(e)}"

  elif name == "read_file":
    filepath = arguments.get("filepath", "")
    start_line = arguments.get("start_line")
    end_line = arguments.get("end_line")

    try:
      with open(filepath, "r", encoding="utf-8") as f:
        lines = f.readlines()

      if start_line is not None or end_line is not None:
        start_idx = (start_line - 1) if (start_line is not None and start_line > 0) else 0
        end_idx = end_line if end_line is not None else len(lines)
        
        selected_lines = lines[start_idx:end_idx]
        actual_start = start_idx + 1
        actual_end = start_idx + len(selected_lines)
        
        header = f"[Lines {actual_start}-{actual_end} of {filepath} (total {len(lines)} lines)]\n"
        content = header + "".join(selected_lines)
        return truncate_output(content, max_chars=8000)
      else:
        return truncate_output("".join(lines), max_chars=8000)

    except Exception as e:
      return f"Error reading file: {str(e)}"

  elif name == "fetch_webpage":
    url = arguments.get("url", "")
    print(f"\n🌐 Fetching webpage: {url}\n" + "-" * 40)
    try:
      import urllib.request
      from html.parser import HTMLParser

      req = urllib.request.Request(
          url,
          headers={"User-Agent": "Mozilla/5.0 (compatible; CorindoCLI/1.0)"}
      )
      with urllib.request.urlopen(req, timeout=15) as response:
        html_content = response.read().decode("utf-8", errors="ignore")

      class HTMLFilter(HTMLParser):
        def __init__(self):
          super().__init__()
          self.text = []
        def handle_data(self, data):
          self.text.append(data)

      parser = HTMLFilter()
      parser.feed(html_content)
      text_content = "".join(parser.text)
      cleaned_text = "\n".join([line.strip() for line in text_content.splitlines() if line.strip()])

      print("-" * 40)
      return truncate_output(cleaned_text, max_chars=8000)
    except Exception as e:
      return f"Error fetching webpage: {str(e)}"

  return f"Error: Unknown tool '{name}'"


# REPL Main Loop
def main():
  # Setup config and history directory under ~/.config/corindo-cli/
  config_dir = pathlib.Path.home() / ".config" / "corindo-cli"
  config_dir.mkdir(parents=True, exist_ok=True)
  history_file = config_dir / ".history"

  if history_file.exists():
    readline.read_history_file(history_file)

  print(f"🤖 Corindo CLI Agent started! Using model: {MODEL_NAME}")
  print(f"📁 Config loaded from: {loaded_path}")
  print("Type 'exit' or 'quit' to exit.\n")

  messages = [
      {
          "role": "system",
          "content": (
              "You are a capable terminal and development assistant"
              " agent. You can read/write files, fetch web pages, and run shell commands (e.g.,"
              " debuild, git, cargo) to help the user complete software"
              " engineering tasks. Always reason clearly before taking"
              " actions."
          ),
      }
  ]

  while True:
    try:
      user_input = input("\n👉 You: ")
      if user_input.lower() in ["exit", "quit"]:
        break
      if not user_input.strip():
        continue

      messages.append({"role": "user", "content": user_input})

      # Ensure that the tool_denied state is reset at the start of
      # each new dialogue round.
      tool_denied = False

      response = client.chat.completions.create(
          model=MODEL_NAME, messages=messages, tools=tools, tool_choice="auto"
      )

      response_message = response.choices[0].message
      messages.append(response_message)

      while response_message.tool_calls:
        tool_denied = False

        for tool_call in response_message.tool_calls:
          function_name = tool_call.function.name
          function_args = json.loads(tool_call.function.arguments)

          print(
              f"⚙️  Calling tool: {function_name}("
              f"{json.dumps(function_args, ensure_ascii=False)})"
          )

          tool_output = execute_tool(function_name, function_args)

          messages.append({
              "role": "tool",
              "tool_call_id": tool_call.id,
              "name": function_name,
              "content": tool_output,
          })

          if "Execution cancelled" in tool_output:
            tool_denied = True
            break

        if tool_denied:
          response = client.chat.completions.create(
              model=MODEL_NAME, messages=messages
          )
          response_message = response.choices[0].message
          messages.append(response_message)

          # Ensure that the AI's response to refusals is printed.
          if response_message.content:
            print(f"\n🤖 Agent:\n{response_message.content}")

          break

        response = client.chat.completions.create(
            model=MODEL_NAME, messages=messages, tools=tools, tool_choice="auto"
        )
        response_message = response.choices[0].message
        messages.append(response_message)

      if response_message.content and not tool_denied:
        print(f"\n🤖 Agent:\n{response_message.content}")

      readline.write_history_file(history_file)

    except KeyboardInterrupt:
      print("\nGoodbye!")
      break
    except Exception as e:
      print(f"\n❌ Error occurred: {e}")


if __name__ == "__main__":
  main()
