import hashlib
import io
import os
import re
import requests
import socket
import subprocess
import sys
import typer
import webbrowser
import zipfile

from datetime import datetime
from rich.console import Console

from .vrignore import load_vrignore_patterns, matches_vrignore
from .config import (
    API_BASE_URL,
    EXT_TO_LANG,
    SUCCESS_STYLE,
    WARNING_STYLE,
    ERROR_STYLE,
    INFO_STYLE,
    get_cookie_file,
    get_api_key_file,
    ensure_voicerun_dir,
    get_organization_id,
)


console = Console()


def camel_to_snake_case(camel_string: str) -> str:
    """Convert camelCase string to snake_case."""
    return re.sub(r"(?<!^)(?=[A-Z])", "_", camel_string).lower()


def convert_dict_keys_to_snake_case(data: dict) -> dict:
    """Convert all keys in a dictionary from camelCase to snake_case."""
    snake_case_dict = {}
    for key, value in data.items():
        snake_key = camel_to_snake_case(key)
        snake_case_dict[snake_key] = value
    return snake_case_dict


def format_phone_number(number):
    if not number:
        return ""
    # Remove all non-digit characters
    digits = re.sub(r"\D", "", number)
    if len(digits) == 11 and digits.startswith("1"):
        # US number with country code
        return f"+1 ({digits[1:4]}) {digits[4:7]}-{digits[7:]}"
    elif len(digits) == 10:
        # US number without country code
        return f"({digits[0:3]}) {digits[3:6]}-{digits[6:]}"
    elif len(digits) == 11:
        # International number with country code
        return f"+{digits[0]} {digits[1:4]} {digits[4:7]}-{digits[7:]}"
    else:
        # Fallback: just return the original
        return number


def save_cookie(cookie: str):
    """Save authentication cookie for the current context."""
    ensure_voicerun_dir()
    cookie_file = get_cookie_file(for_write=True)
    with open(cookie_file, "w") as f:
        f.write(cookie)


def load_cookie() -> str:
    """Load authentication cookie for the current context."""
    cookie_file = get_cookie_file()
    if cookie_file.exists():
        with open(cookie_file, "r") as f:
            cookie = f.read().strip()
            return cookie

    return ""


def save_api_key(api_key: str):
    """Save API key for the current context."""
    ensure_voicerun_dir()
    api_key_file = get_api_key_file(for_write=True)
    with open(api_key_file, "w") as f:
        f.write(api_key)


def load_api_key() -> str:
    """Load API key for the current context."""
    api_key_file = get_api_key_file()
    if api_key_file.exists():
        with open(api_key_file, "r") as f:
            api_key = f.read().strip()
            return api_key

    return ""


def make_request(endpoint: str, method="GET", headers=None, include_org_id=True, **kwargs):
    # Prioritize API key over cookie
    api_key = load_api_key()
    cookie = load_cookie()

    headers = headers or {}
    headers = {
        "Content-Type": "application/json",
        **headers,
    }

    if api_key:
        headers["Authorization"] = f"Bearer {api_key}"
    elif cookie:
        headers["Cookie"] = cookie

    # Add organization ID header if set in context
    if include_org_id:
        org_id = get_organization_id()
        if org_id:
            headers["organization-id"] = org_id

    if os.getenv("DEBUG"):
        print_info(
            f"Making {method} request to {API_BASE_URL}{endpoint} with json: {kwargs.get('json')}"
        )

    try:
        response = requests.request(
            method, f"{API_BASE_URL}{endpoint}", headers=headers, **kwargs
        )
        response.raise_for_status()

        if response.status_code == 200 or response.status_code == 201:
            return response.json()

        if response.status_code == 204:
            return True
    except requests.HTTPError as e:
        handle_http_error(e)
        return None


def format_date(date_str):
    """Format date string to YYYY-MM-DD."""
    if not date_str or date_str == "N/A":
        return ""
    try:
        return datetime.fromisoformat(date_str).strftime("%Y-%m-%d")
    except Exception:
        return date_str


def get_open_port():
    """Get an open port on the machine."""
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.bind(("", 0))
        s.listen(1)
        port = s.getsockname()[1]
    return port


def open_browser(url):
    """Open a URL in the default browser."""
    try:
        webbrowser.open(url)
        return None
    except Exception as e:
        return e


def print_success(message: str):
    """Print a success message using the configured style."""
    console.print(f"[{SUCCESS_STYLE}]{message}[/{SUCCESS_STYLE}]")


def print_warning(message: str):
    """Print a warning message with 'Warning:' prefix using the configured style."""
    console.print(f"[{WARNING_STYLE}]Warning: {message}[/{WARNING_STYLE}]")


def print_error(message: str):
    """Print an error message with 'Error:' prefix using the configured style."""
    console.print(f"[{ERROR_STYLE}]Error: {message}[/{ERROR_STYLE}]")


def print_info(message: str):
    """Print an info/progress message using the configured style."""
    console.print(f"[{INFO_STYLE}]{message}[/{INFO_STYLE}]")

def print_standard(message: str, *args, **kwargs):
    """Print a standard message using the configured style."""
    console.print(message, *args, **kwargs)


def print_table(headers: list[str], rows: list[list[str]]):
    """Print a column-aligned table like kubectl get pods."""
    gap = 3
    col_widths = [len(h) for h in headers]
    for row in rows:
        for i, cell in enumerate(row):
            col_widths[i] = max(col_widths[i], len(cell))
    print("".join(h.ljust(col_widths[i] + gap) for i, h in enumerate(headers)).rstrip())
    for row in rows:
        print("".join(cell.ljust(col_widths[i] + gap) for i, cell in enumerate(row)).rstrip())


def handle_http_error(e: requests.HTTPError):
    """Handle HTTP errors by printing a user-friendly message."""
    if e.response.status_code == 401 or e.response.status_code == 403:
        print_error("Unauthorized. Please sign in with `vr signin`.")
    else:
        try:
            detail = e.response.json().get("detail", e.response.text)
            print_error(detail)
        except requests.exceptions.JSONDecodeError:
            print_error(str(e))


def get_authenticated_session() -> requests.Session:
    """Get an authenticated session."""
    session = requests.Session()

    # Prioritize API key over cookie
    api_key = load_api_key()
    if api_key:
        session.headers["Authorization"] = f"Bearer {api_key}"
    else:
        cookie = load_cookie()
        if cookie:
            session.headers["Cookie"] = cookie

    return session


def is_uuid(uuid_string):
    """Check if a string looks like a UUID."""
    uuid_pattern = re.compile(
        r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.IGNORECASE
    )
    return bool(uuid_pattern.match(uuid_string))


def create_venv(directory_path: str):
    """Create a virtual environment in the directory using uv and install requirements."""
    # Convert to absolute path
    directory_path = os.path.abspath(directory_path)
    venv_path = os.path.join(directory_path, ".venv")
    pyproject_path = os.path.join(directory_path, "pyproject.toml")
    requirements_path = os.path.join(directory_path, "requirements.txt")

    # Check if .venv already exists
    if os.path.exists(venv_path):
        print_info(f"Virtual environment already exists at {venv_path}")
        return

    try:
        # First check for pyproject.toml and use uv sync
        if os.path.exists(pyproject_path):
            print_info(
                f"Found pyproject.toml, creating virtual environment with uv sync"
            )

            # Use uv sync to create virtual environment and install dependencies
            result = subprocess.run(
                ["uv", "sync", "--python", "3.11"],
                capture_output=True,
                text=True,
                cwd=directory_path,
            )

            if result.returncode != 0:
                print_error(
                    f"Failed to create virtual environment with uv sync: {result.stderr}"
                )
                return

            print_success(
                f"Virtual environment created and dependencies installed in {venv_path}"
            )
            return

        # Fall back to requirements.txt if pyproject.toml doesn't exist
        elif os.path.exists(requirements_path):
            print_info(
                f"Found requirements.txt, creating virtual environment with uv pip"
            )

            # First create the virtual environment
            print_info(f"Creating virtual environment in {venv_path}")
            result = subprocess.run(
                ["uv", "venv", venv_path],
                capture_output=True,
                text=True,
                cwd=directory_path,
            )

            if result.returncode != 0:
                print_error(f"Failed to create virtual environment: {result.stderr}")
                return

            # Get the Python interpreter path in the virtual environment
            if os.name == "nt":  # Windows
                python_path = os.path.join(venv_path, "Scripts", "python.exe")
            else:  # Unix/Linux/macOS
                python_path = os.path.join(venv_path, "bin", "python")

            # Install requirements using uv pip with the virtual environment's Python
            print_info("Installing dependencies from requirements.txt")
            result = subprocess.run(
                [
                    "uv",
                    "pip",
                    "install",
                    "-r",
                    "requirements.txt",
                    "--python",
                    python_path,
                ],
                capture_output=True,
                text=True,
                cwd=directory_path,
            )

            if result.returncode != 0:
                print_error(f"Failed to install dependencies: {result.stderr}")
                return

            print_success(
                f"Virtual environment created and dependencies installed in {venv_path}"
            )
        else:
            print_info(
                f"No pyproject.toml or requirements.txt found in {directory_path}, "
                f"skipping virtual environment creation."
            )
            return

    except FileNotFoundError:
        print_error(
            "uv is not installed. Please install uv first: "
            "https://docs.astral.sh/uv/getting-started/installation/"
        )
    except Exception as e:
        print_error(f"Error creating virtual environment: {str(e)}")


def get_lang(directory_path: str):
    """Get the language of the function code."""
    handler_file = next(
        (f for f in os.listdir(directory_path) if f.startswith("handler.")),
        "handler.py",
    )
    ext = os.path.splitext(handler_file)[-1].lstrip(".")
    return EXT_TO_LANG.get(ext, "python")


EXCLUDE_PATTERNS = [
    ".venv",  # Virtual environment
    "__pycache__",  # Python cache
    ".pyc",  # Compiled Python files
    ".DS_Store",  # macOS system files
    "Thumbs.db",  # Windows system files
    ".git",  # Git directory
    ".gitignore",  # Git ignore file
    "*.zip",  # Existing zip files
]


def _should_exclude(path):
    """Check if a path should be excluded from packaging/checksumming."""
    basename = os.path.basename(path)
    for pattern in EXCLUDE_PATTERNS:
        if pattern.startswith("*"):
            if path.endswith(pattern[1:]):
                return True
        elif basename == pattern:
            return True
    return False


def package_function(directory_path: str) -> tuple[bytes, str]:
    """Create a zip archive of the directory and return the bytes and language."""
    # Convert to absolute path and ensure it exists
    directory_path = os.path.abspath(directory_path)

    lang = get_lang(directory_path)

    if not os.path.exists(directory_path):
        raise FileNotFoundError(f"Directory '{directory_path}' does not exist")

    vrignore_patterns = load_vrignore_patterns(directory_path)

    # Create the zip file in memory
    zip_buffer = io.BytesIO()
    with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zipf:
        for root, dirs, files in os.walk(directory_path):
            # Filter out excluded directories
            dirs[:] = [
                d for d in dirs
                if not _should_exclude(d)
                and not matches_vrignore(
                    os.path.relpath(os.path.join(root, d), directory_path),
                    vrignore_patterns,
                    is_dir=True,
                )
            ]

            for file in files:
                file_path = os.path.join(root, file)

                # Skip excluded files
                if _should_exclude(file_path):
                    continue

                arcname = os.path.relpath(file_path, directory_path)

                # Skip files matching .vrignore patterns
                if matches_vrignore(arcname, vrignore_patterns):
                    continue

                zipf.write(file_path, arcname=arcname)

    zip_bytes = zip_buffer.getvalue()
    return zip_bytes, lang


def calculate_project_checksum(directory_path: str) -> str:
    """Calculate a SHA-256 checksum of all project files.

    Walks the same files as package_function (using shared exclusion patterns)
    and produces a stable hash by sorting file paths.
    """
    directory_path = os.path.abspath(directory_path)

    if not os.path.exists(directory_path):
        raise FileNotFoundError(f"Directory '{directory_path}' does not exist")

    vrignore_patterns = load_vrignore_patterns(directory_path)
    file_hashes = []

    for root, dirs, files in os.walk(directory_path):
        dirs[:] = sorted(
            d for d in dirs
            if not _should_exclude(d)
            and not matches_vrignore(
                os.path.relpath(os.path.join(root, d), directory_path),
                vrignore_patterns,
                is_dir=True,
            )
        )

        for file in sorted(files):
            file_path = os.path.join(root, file)

            if _should_exclude(file_path):
                continue

            rel_path = os.path.relpath(file_path, directory_path)

            if matches_vrignore(rel_path, vrignore_patterns):
                continue

            file_hash = hashlib.sha256()
            try:
                with open(file_path, "rb") as f:
                    for chunk in iter(lambda: f.read(8192), b""):
                        file_hash.update(chunk)
            except (OSError, PermissionError):
                continue

            file_hashes.append(f"{rel_path}:{file_hash.hexdigest()}")

    project_hash = hashlib.sha256("\n".join(file_hashes).encode()).hexdigest()
    return project_hash


def not_empty(value: str) -> str:
    """Check if a value is not empty."""
    if not value or not value.strip():
        raise typer.BadParameter("Value cannot be empty.")
    return value


def prompt(message: str, default=None, validation=not_empty, **kwargs):
    """Prompt for input with validation (default: not_empty) and consistent style."""
    return typer.prompt(message, default=default, value_proc=validation, **kwargs)


def confirm(message: str, default=None, validation=None, **kwargs):
    """Prompt for confirmation with consistent style (no validation by default)."""
    return typer.confirm(message, default=default, **kwargs)


def is_agent() -> bool:
    """Detect if the CLI is being run by an AI coding agent.

    Checks for a standardized AGENT or AI_AGENT env var first, then falls
    back to tool-specific variables set by known coding agents.
    """
    env = os.environ
    # Standardized (proposed) env vars
    if env.get("AI_AGENT") or env.get("AGENT"):
        return True
    # Tool-specific env vars
    agent_vars = [
        "CLAUDECODE",       # Claude Code
        "CURSOR_AGENT",     # Cursor
        "GEMINI_CLI",       # Gemini CLI
        "CODEX_SANDBOX",    # Codex CLI
        "AUGMENT_AGENT",    # Augment
        "CLINE_ACTIVE",     # Cline
        "OPENCODE_CLIENT",  # OpenCode
        "TRAE_AI_SHELL_ID", # TRAE AI
        "GOOSE_TERMINAL",   # Goose
    ]
    return any(env.get(var) for var in agent_vars)


def should_output_json(json_flag: bool = False, table_flag: bool = False) -> bool:
    """Determine whether to output JSON based on flags and terminal detection.

    - --json flag forces JSON output
    - --table flag forces table/rich output
    - Otherwise, auto-detect: JSON when not a TTY or when an AI agent is detected
    """
    if json_flag:
        return True
    if table_flag:
        return False
    return not sys.stdout.isatty() or is_agent()


def print_json(data):
    """Print data as formatted JSON to stdout."""
    import json
    print(json.dumps(data, indent=2, default=str))


def format_duration_ms(ms) -> str:
    """Format milliseconds as human-readable duration."""
    if ms is None:
        return "N/A"
    try:
        ms = float(ms)
    except (TypeError, ValueError):
        return str(ms)
    if ms >= 60000:
        minutes = int(ms // 60000)
        seconds = int((ms % 60000) // 1000)
        return f"{minutes}m {seconds}s"
    elif ms >= 1000:
        seconds = ms / 1000
        return f"{seconds:.1f}s"
    else:
        return f"{int(ms)}ms"
