import json
from typing import Dict, List, Tuple

from .images import adjust_coordinates
from .parser import (
    extract_action_line,
    iter_tool_call_params,
    looks_infeasible_response,
    parse_coordinate,
    parse_keys,
    parse_number,
)


def py_string(text: str) -> str:
    return json.dumps("" if text is None else str(text), ensure_ascii=False)


def _termination_code(status: object) -> str:
    normalized = str(status or "success").strip().lower()
    return "FAIL" if normalized in {"fail", "failed", "failure", "error", "infeasible"} else "DONE"


def _coord_adjuster(
    *,
    coordinate_type: str,
    original_width: int = None,
    original_height: int = None,
    processed_width: int = None,
    processed_height: int = None,
):
    def adjust(x: float, y: float) -> Tuple[int, int]:
        return adjust_coordinates(
            x,
            y,
            coordinate_type=coordinate_type,
            original_width=original_width,
            original_height=original_height,
            processed_width=processed_width,
            processed_height=processed_height,
        )

    return adjust


def parse_base_response(
    response: str,
    *,
    coordinate_type: str,
    original_width: int = None,
    original_height: int = None,
    processed_width: int = None,
    processed_height: int = None,
) -> Tuple[str, List[str]]:
    low_level_instruction = ""
    pyautogui_code: List[str] = []

    if not response or not response.strip():
        return low_level_instruction, pyautogui_code

    adjust = _coord_adjuster(
        coordinate_type=coordinate_type,
        original_width=original_width,
        original_height=original_height,
        processed_width=processed_width,
        processed_height=processed_height,
    )

    def process_tool_call_params(params: Dict) -> None:
        action = params.get("action")
        if not action:
            return

        coordinate = parse_coordinate(params.get("coordinate"))
        text = params.get("text")

        def press_modifier_keys() -> None:
            if text:
                for key in str(text).split("+"):
                    key = key.strip().lower()
                    if key:
                        pyautogui_code.append(f"pyautogui.keyDown({py_string(key)})")

        def release_modifier_keys() -> None:
            if text:
                keys = [key.strip().lower() for key in str(text).split("+") if key.strip()]
                for key in reversed(keys):
                    pyautogui_code.append(f"pyautogui.keyUp({py_string(key)})")

        if action == "left_click":
            press_modifier_keys()
            if coordinate:
                x, y = adjust(*coordinate)
                pyautogui_code.append(f"pyautogui.click({x}, {y})")
            else:
                pyautogui_code.append("pyautogui.click()")
            release_modifier_keys()
        elif action == "right_click":
            press_modifier_keys()
            if coordinate:
                x, y = adjust(*coordinate)
                pyautogui_code.append(f"pyautogui.rightClick({x}, {y})")
            else:
                pyautogui_code.append("pyautogui.rightClick()")
            release_modifier_keys()
        elif action == "middle_click":
            press_modifier_keys()
            if coordinate:
                x, y = adjust(*coordinate)
                pyautogui_code.append(f"pyautogui.middleClick({x}, {y})")
            else:
                pyautogui_code.append("pyautogui.middleClick()")
            release_modifier_keys()
        elif action == "double_click":
            press_modifier_keys()
            if coordinate:
                x, y = adjust(*coordinate)
                pyautogui_code.append(f"pyautogui.doubleClick({x}, {y})")
            else:
                pyautogui_code.append("pyautogui.doubleClick()")
            release_modifier_keys()
        elif action == "triple_click":
            press_modifier_keys()
            if coordinate:
                x, y = adjust(*coordinate)
                pyautogui_code.append(f"pyautogui.doubleClick({x}, {y})")
            else:
                pyautogui_code.append("pyautogui.doubleClick()")
            release_modifier_keys()
        elif action == "type":
            text = params.get("text", "")
            pyautogui_code.append(f"pyautogui.typewrite({py_string(text)})")
        elif action == "key":
            keys = parse_keys(params.get("keys", []))
            keys_str = ", ".join(py_string(key) for key in keys)
            if len(keys) > 1:
                pyautogui_code.append(f"pyautogui.hotkey({keys_str})")
            else:
                pyautogui_code.append(f"pyautogui.press({keys_str})")
        elif action in {"scroll", "hscroll"}:
            press_modifier_keys()
            pixels = params.get("pixels", 0)
            try:
                pixels = int(float(pixels))
            except Exception:
                pixels = 0
            pyautogui_code.append(f"pyautogui.scroll({pixels})")
            release_modifier_keys()
        elif action == "wait":
            pyautogui_code.append("WAIT")
        elif action == "terminate":
            pyautogui_code.append(_termination_code(params.get("status", "success")))
        elif action == "answer":
            pyautogui_code.append("DONE")
        elif action == "mouse_move":
            if coordinate:
                x, y = adjust(*coordinate)
                pyautogui_code.append(f"pyautogui.moveTo({x}, {y})")
            else:
                pyautogui_code.append("pyautogui.moveTo(0, 0)")
        elif action == "left_click_drag":
            if coordinate:
                x, y = adjust(*coordinate)
                duration = 0.5
                if "duration" in params:
                    try:
                        duration = float(params["duration"])
                    except Exception:
                        duration = 0.5
                pyautogui_code.append(f"pyautogui.dragTo({x}, {y}, duration={duration})")
            else:
                pyautogui_code.append("pyautogui.dragTo(0, 0)")

    low_level_instruction = extract_action_line(response, preserve_base_split_bug=True)

    for params in iter_tool_call_params(response):
        process_tool_call_params(params)

    if not low_level_instruction and pyautogui_code:
        low_level_instruction = _instruction_from_first_code(pyautogui_code[0])

    return low_level_instruction, pyautogui_code


def parse_internal_response(
    response: str,
    *,
    coordinate_type: str,
    original_width: int = None,
    original_height: int = None,
    processed_width: int = None,
    processed_height: int = None,
) -> Tuple[str, List[str]]:
    low_level_instruction = ""
    pyautogui_code: List[str] = []

    if not response or not response.strip():
        return low_level_instruction, pyautogui_code

    adjust = _coord_adjuster(
        coordinate_type=coordinate_type,
        original_width=original_width,
        original_height=original_height,
        processed_width=processed_width,
        processed_height=processed_height,
    )
    infeasible_response = looks_infeasible_response(response)

    def process_tool_call_params(params: Dict) -> None:
        action = params.get("action")
        if not action:
            return

        coordinate = parse_coordinate(params.get("coordinate"))

        if action == "left_click":
            if coordinate:
                x, y = adjust(*coordinate)
                pyautogui_code.append(f"pyautogui.click({x}, {y})")
            else:
                pyautogui_code.append("pyautogui.click()")
        elif action == "right_click":
            if coordinate:
                x, y = adjust(*coordinate)
                pyautogui_code.append(f"pyautogui.rightClick({x}, {y})")
            else:
                pyautogui_code.append("pyautogui.rightClick()")
        elif action == "middle_click":
            if coordinate:
                x, y = adjust(*coordinate)
                pyautogui_code.append(f"pyautogui.middleClick({x}, {y})")
            else:
                pyautogui_code.append("pyautogui.middleClick()")
        elif action == "double_click":
            if coordinate:
                x, y = adjust(*coordinate)
                pyautogui_code.append(f"pyautogui.doubleClick({x}, {y})")
            else:
                pyautogui_code.append("pyautogui.doubleClick()")
        elif action == "triple_click":
            if coordinate:
                x, y = adjust(*coordinate)
                pyautogui_code.append(f"pyautogui.tripleClick({x}, {y})")
            else:
                pyautogui_code.append("pyautogui.tripleClick()")
        elif action == "type":
            text = "" if params.get("text") is None else str(params.get("text", ""))
            normalized_text = text.replace("\r\n", "\n").replace("\r", "\n")
            if "\n" not in normalized_text:
                pyautogui_code.append(f"pyautogui.typewrite({py_string(normalized_text)})")
            else:
                chunks = normalized_text.split("\n")
                for idx, chunk in enumerate(chunks):
                    if chunk:
                        pyautogui_code.append(f"pyautogui.typewrite({py_string(chunk)})")
                    if idx < len(chunks) - 1:
                        pyautogui_code.append(f"pyautogui.press({py_string('enter')})")
        elif action == "key":
            keys = parse_keys(params.get("keys", []), lowercase=True)
            if not keys:
                return
            keys_str = ", ".join(py_string(key) for key in keys)
            if len(keys) > 1:
                pyautogui_code.append(f"pyautogui.hotkey({keys_str})")
            else:
                pyautogui_code.append(f"pyautogui.press({keys_str})")
        elif action == "key_down":
            for key in parse_keys(params.get("keys", []), lowercase=True):
                pyautogui_code.append(f"pyautogui.keyDown({py_string(key)})")
        elif action == "key_up":
            for key in parse_keys(params.get("keys", []), lowercase=True):
                pyautogui_code.append(f"pyautogui.keyUp({py_string(key)})")
        elif action == "scroll":
            pixels = int(parse_number(params.get("pixels", 0), default=0))
            pyautogui_code.append(f"pyautogui.scroll({pixels})")
        elif action == "hscroll":
            pixels = int(parse_number(params.get("pixels", 0), default=0))
            pyautogui_code.append(f"pyautogui.hscroll({pixels})")
        elif action == "wait":
            pyautogui_code.append("WAIT")
        elif action == "terminate":
            pyautogui_code.append(_termination_code(params.get("status", "success")))
        elif action == "call_user":
            pyautogui_code.append("FAIL" if infeasible_response else "DONE")
        elif action == "screenshot":
            pyautogui_code.append("WAIT")
        elif action == "mouse_move":
            if coordinate:
                x, y = adjust(*coordinate)
                pyautogui_code.append(f"pyautogui.moveTo({x}, {y})")
            else:
                pyautogui_code.append("pyautogui.moveTo(0, 0)")
        elif action == "left_click_drag":
            if coordinate:
                x, y = adjust(*coordinate)
                duration = parse_number(params.get("duration", 0.5), default=0.5)
                pyautogui_code.append(f"pyautogui.dragTo({x}, {y}, duration={duration})")
            else:
                pyautogui_code.append("pyautogui.dragTo(0, 0)")
        elif action == "left_mouse_down":
            if coordinate:
                x, y = adjust(*coordinate)
                pyautogui_code.append(f"pyautogui.moveTo({x}, {y})")
            pyautogui_code.append("pyautogui.mouseDown(button='left')")
        elif action == "left_mouse_up":
            if coordinate:
                x, y = adjust(*coordinate)
                pyautogui_code.append(f"pyautogui.moveTo({x}, {y})")
            pyautogui_code.append("pyautogui.mouseUp(button='left')")

    low_level_instruction = extract_action_line(response)

    for params in iter_tool_call_params(response):
        process_tool_call_params(params)

    if not pyautogui_code:
        pyautogui_code.append("FAIL" if infeasible_response else "DONE")

    if not low_level_instruction and pyautogui_code:
        first_code = pyautogui_code[0]
        if first_code == "FAIL":
            low_level_instruction = "Need user input"
        else:
            low_level_instruction = _instruction_from_first_code(first_code)

    return low_level_instruction, pyautogui_code


def _instruction_from_first_code(first_code: str) -> str:
    if first_code == "DONE":
        return "Task completed"
    if first_code == "WAIT":
        return "Waiting"
    if "." in first_code:
        return f"Performing {first_code.split('.', 1)[1].split('(', 1)[0]} action"
    return "Performing action"
