#  Copyright 2025 Google LLC
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

import asyncio
import logging
import os

from google import genai
from google.genai.types import (
    ComputerUse,
    Content,
    Environment,
    FinishReason,
    FunctionResponse,
    FunctionResponseBlob,
    GenerateContentConfig,
    Part,
    ThinkingConfig,
    Tool,
)
from playwright.async_api import Page, async_playwright

logging.getLogger("google_genai._common").setLevel(logging.ERROR)

# --- CONFIGURATION ---
# Load configuration from environment variables for best practice.
PROJECT_ID = os.environ.get("GOOGLE_CLOUD_PROJECT")
LOCATION = os.environ.get("GOOGLE_LOCATION", "global")
MODEL_ID = os.environ.get("MODEL_ID", "gemini-3.5-flash")


# --- HELPER FUNCTIONS  ---


def normalize_x(x: int, screen_width: int) -> int:
    """Convert normalized x coordinate (0-1000) to actual pixel coordinate."""
    return int(x / 1000 * screen_width)


def normalize_y(y: int, screen_height: int) -> int:
    """Convert normalized y coordinate (0-1000) to actual pixel coordinate."""
    return int(y / 1000 * screen_height)


async def execute_function_calls(
    response, page: Page, screen_width: int, screen_height: int
) -> tuple[
    str, list[tuple[str, str, bool]]
]:  # <-- Note the added bool for safety status
    """Extracts and executes function calls from the model response."""
    await asyncio.sleep(0.1)

    function_calls = [
        part.function_call
        for part in response.candidates[0].content.parts
        if hasattr(part, "function_call") and part.function_call
    ]

    thoughts = [
        part.text
        for part in response.candidates[0].content.parts
        if hasattr(part, "text") and part.text
    ]

    if thoughts:
        print(f"🤔 Model Reasoning: {' '.join(thoughts)}")

    if not function_calls:
        return "NO_ACTION", []

    results = []
    for function_call in function_calls:
        result = None
        safety_acknowledged = False

        safety_decision = function_call.args.get("safety_decision")
        if (
            safety_decision
            and safety_decision.get("decision") == "require_confirmation"
        ):
            print(f"\n⚠️ SAFETY PROMPT: {safety_decision.get('explanation')}")
            user_input = input(
                f"Allow the agent to execute '{function_call.name}'? (y/n): "
            )

            if user_input.strip().lower() not in ["y", "yes"]:
                print("🚫 Action denied by user.")
                results.append((function_call.name, "user_denied", False))
                continue  # Skip execution and move to the next function call

            print("✅ Action approved.")
            safety_acknowledged = True

        print(f"⚡ Executing Action: {function_call.name}")
        try:
            if function_call.name == "open_web_browser":
                result = "success"  # The browser is already open
            elif function_call.name == "navigate":
                await page.goto(function_call.args["url"])
                result = "success"
            elif function_call.name == "click_at":
                actual_x = normalize_x(function_call.args["x"], screen_width)
                actual_y = normalize_y(function_call.args["y"], screen_height)
                await page.mouse.click(actual_x, actual_y)
                result = "success"
            elif function_call.name == "type_text_at":
                text_to_type = function_call.args["text"]
                print(f'[DEBUG] Typing text: "{text_to_type}"')
                actual_x = normalize_x(function_call.args["x"], screen_width)
                actual_y = normalize_y(function_call.args["y"], screen_height)
                await page.mouse.click(actual_x, actual_y)
                await asyncio.sleep(0.1)
                await page.keyboard.type(text_to_type)
                if function_call.args.get("press_enter", False):
                    await page.keyboard.press("Enter")
                result = "success"
            else:
                result = "unknown_function"
        except Exception as e:
            print(f"❗️ Error executing {function_call.name}: {e}")
            result = f"error: {e!s}"

        results.append((function_call.name, result, safety_acknowledged))

    return "CONTINUE", results


# --- THE AGENT LOOP ---


async def agent_loop(initial_prompt: str, max_turns: int = 20) -> None:
    """Main agent loop for local execution with a browser."""
    if not PROJECT_ID:
        raise ValueError("GOOGLE_PROJECT_ID environment variable not set.")

    client = genai.Client(vertexai=True, project=PROJECT_ID, location=LOCATION)

    browser = None
    try:
        async with async_playwright() as p:
            # MODIFIED: Launch browser in a try...finally block
            browser = await p.chromium.launch(headless=False)
            page = await browser.new_page()
            sw, sh = 960, 1080
            await page.set_viewport_size({"width": sw, "height": sh})
            await page.goto("https://www.google.com")

            print(f"🎬 Starting Agent Loop with prompt: '{initial_prompt}'")

            # Configure Computer Use tool with browser environment
            # Base configuration for the Computer Use tool
            config_kwargs = {
                "tools": [
                    Tool(
                        computer_use=ComputerUse(
                            environment=Environment.ENVIRONMENT_BROWSER,
                            # Optional: Exclude specific predefined functions
                            excluded_predefined_functions=["drag_and_drop"],
                        )
                    )
                ]
            }

            # Conditionally add thinking_config only for the Gemini 3 models
            model_version = float(MODEL_ID.split("-")[1])
            if model_version >= 3:
                config_kwargs["thinking_config"] = ThinkingConfig(include_thoughts=True)

            config = GenerateContentConfig(**config_kwargs)

            screenshot = await page.screenshot()
            contents = [
                Content(
                    role="user",
                    parts=[
                        Part(text=initial_prompt),
                        Part.from_bytes(data=screenshot, mime_type="image/png"),
                    ],
                )
            ]
            for turn in range(max_turns):
                print(f"\n--- 🔁 Turn {turn + 1} ---")
                print(f"[DEBUG] Current URL: {page.url}")

                response = client.models.generate_content(
                    model=MODEL_ID, contents=contents, config=config
                )

                if not response.candidates:
                    print("❗️ Model returned no candidates. Terminating loop.")
                    print("Full Response:", response)
                    break

                if response.candidates[0].finish_reason == FinishReason.SAFETY:
                    print(
                        "🛑 SAFETY TRIGGERED: The model halted execution due to safety policies."
                    )
                    print(f"Details: {response.candidates[0].safety_ratings}")
                    break

                print(
                    f"[DEBUG] Model Finish Reason: {response.candidates[0].finish_reason}"
                )
                contents.append(response.candidates[0].content)
                print("[DEBUG] Appended model response to history.")

                # Check if the attribute exists AND is not None
                active_function_calls = [
                    part.function_call
                    for part in response.candidates[0].content.parts
                    if hasattr(part, "function_call") and part.function_call
                ]

                if not active_function_calls:
                    final_text = "".join(
                        part.text
                        for part in response.candidates[0].content.parts
                        if hasattr(part, "text") and part.text is not None
                    )
                    if final_text:
                        print(f"✅ Agent Finished: {final_text}")
                        break

                status, execution_results = await execute_function_calls(
                    response, page, sw, sh
                )
                print(
                    f"[DEBUG] Execution Results: status='{status}', results={execution_results}"
                )

                if status == "NO_ACTION":
                    continue

                function_response_parts = []
                # Unpack the 3 variables returned by our updated function
                for name, result, safety_acknowledged in execution_results:
                    screenshot = await page.screenshot()
                    current_url = page.url

                    # Prepare the response payload
                    response_payload = {"url": current_url}

                    # Handle the safety and denial states
                    if result == "user_denied":
                        response_payload["error"] = "user_denied"
                    elif safety_acknowledged:
                        # CRITICAL: Acknowledge the safety decision so the API doesn't throw an error
                        response_payload["safety_acknowledgement"] = True

                    function_response_parts.append(
                        Part(
                            function_response=FunctionResponse(
                                name=name,
                                response=response_payload,
                                parts=[
                                    Part(
                                        inline_data=FunctionResponseBlob(
                                            mime_type="image/png", data=screenshot
                                        )
                                    )
                                ],
                            )
                        )
                    )

                contents.append(Content(role="user", parts=function_response_parts))
                print(f"📝 State captured. History now has {len(contents)} messages.")
    finally:
        if browser:
            await browser.close()
            print("\n--- Browser closed. ---")


# --- SCRIPT ENTRY POINT ---
if __name__ == "__main__":
    prompt = "Navigate to the Google Store and find the page of 'Pixel 10'."

    asyncio.run(agent_loop(prompt))
