#!/usr/bin/env python3
"""
Workflow management commands for n8n-deploy CLI

Provides a consistent 'wf' command group for all wf operations including:
- Basic operations: add, list, delete, search, stats
- Server operations: pull, push, server
"""

import json
import uuid
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

import click
from rich.console import Console
from rich.json import JSON
from rich.table import Table

from ..config import get_config, AppConfig, NOT_PROVIDED
from ..workflow import WorkflowApi
from .app import (
    cli_data_dir_help,
    handle_verbose_flag,
    HELP_DB_FILENAME,
    HELP_FLOW_DIR,
    HELP_JSON,
    HELP_NO_EMOJI,
    HELP_SERVER_URL,
    HELP_TABLE,
    CustomCommand,
    CustomGroup,
)
from .output import (
    cli_error,
    print_workflow_search_table,
    print_workflow_table,
    print_error,
    print_success,
    print_workflow_search_table,
    print_workflow_table,
)

console = Console()


def _read_workflow_file(
    config: AppConfig, workflow_file: str, output_json: bool, no_emoji: bool
) -> Optional[Tuple[Path, Dict[str, Any]]]:
    """Read and parse workflow JSON file.

    Returns:
        Tuple of (file_path, workflow_data) on success, None on error (with JSON output)

    Raises:
        click.Abort: If file not found or invalid JSON (non-JSON mode only)
    """
    file_path = Path(config.workflows_path) / workflow_file

    if not file_path.exists():
        if output_json:
            console.print(JSON.from_data({"success": False, "error": f"Workflow file not found: {file_path}"}))
            return None
        cli_error(f"Workflow file not found: {file_path}", no_emoji)
        raise click.Abort()

    try:
        with open(file_path, "r", encoding="utf-8") as f:
            workflow_data: Dict[str, Any] = json.load(f)
        return file_path, workflow_data
    except json.JSONDecodeError as e:
        if output_json:
            console.print(JSON.from_data({"success": False, "error": f"Invalid JSON in workflow file: {e}"}))
            return None
        cli_error(f"Invalid JSON in workflow file: {e}", no_emoji)
        raise click.Abort()


def _ensure_workflow_id(
    workflow_data: Dict[str, Any], workflow_file: str, output_json: bool, no_emoji: bool
) -> Tuple[str, str]:
    """Ensure workflow has an ID, generating draft if needed.

    Returns:
        Tuple of (workflow_id, workflow_name)
    """
    workflow_id = workflow_data.get("id")
    workflow_name = workflow_data.get("name", workflow_file.replace(".json", ""))

    if not workflow_id:
        workflow_id = f"draft_{uuid.uuid4()}"
        if not output_json and not no_emoji:
            console.print(f"[yellow]⚠️  No ID found in workflow file. Generated draft ID: {workflow_id}[/yellow]")
            console.print("[yellow]    This will be replaced with server-assigned ID after first push.[/yellow]")
        elif not output_json:
            console.print(f"WARNING: No ID found in workflow file. Generated draft ID: {workflow_id}")
            console.print("         This will be replaced with server-assigned ID after first push.")

    return workflow_id, workflow_name


def _resolve_server_for_linking(config: AppConfig, link_remote: str, output_json: bool, no_emoji: bool) -> Tuple[str, int]:
    """Resolve server name or URL for workflow linking.

    Returns:
        Tuple of (server_name, server_id)

    Raises:
        click.Abort: If server not found
    """
    from api.db.servers import ServerCrud

    server_crud = ServerCrud(config=config)

    if "://" in link_remote:
        server = server_crud.get_server_by_url(link_remote)
        error_msg = f"Server URL '{link_remote}' not found in database"
        suggestion = ". Add it with 'server create'"
    else:
        server = server_crud.get_server_by_name(link_remote)
        error_msg = f"Server '{link_remote}' not found in database"
        suggestion = ". Add it with 'server create'"

    if not server:
        if output_json:
            console.print(JSON.from_data({"success": False, "error": error_msg}))
        else:
            cli_error(error_msg + suggestion, no_emoji)
        raise click.Abort()

    server_name = server["name"] if "://" in link_remote else link_remote
    return server_name, server["id"]


def _link_workflow_to_server(
    manager: WorkflowApi, workflow_id: str, server_id: int, server_name: str, output_json: bool, no_emoji: bool
) -> None:
    """Link workflow to server in database."""
    workflow_obj = manager.db.get_workflow(workflow_id)
    if workflow_obj:
        workflow_obj.server_id = server_id
        manager.db.update_workflow(workflow_obj)

    if not output_json:
        if no_emoji:
            console.print(f"Workflow linked to server: {server_name}")
        else:
            console.print(f"🔗 Workflow linked to server: {server_name}")


def _output_add_success(workflow_id: str, workflow_name: str, output_json: bool, no_emoji: bool) -> None:
    """Output success message for workflow add."""
    result = {
        "success": True,
        "workflow_id": workflow_id,
        "workflow_name": workflow_name,
        "message": f"Workflow '{workflow_name}' (ID: {workflow_id}) added to database",
    }

    if output_json:
        console.print(JSON.from_data(result))
    elif no_emoji:
        console.print(f"Workflow '{workflow_name}' (ID: {workflow_id}) added to database")
    else:
        console.print(f"✅ Workflow '{workflow_name}' (ID: {workflow_id}) added to database")


@click.group(cls=CustomGroup)
@click.option(
    "-v",
    "--verbose",
    count=True,
    expose_value=False,
    is_eager=True,
    callback=handle_verbose_flag,
    help="Verbosity level (-v, -vv)",
)
def wf() -> None:
    """🔄 Workflow management commands"""
    pass


# Basic wf operations
@wf.command(cls=CustomCommand)
@click.argument("workflow_file")
@click.option("--data-dir", type=click.Path(), help=cli_data_dir_help)
@click.option("--flow-dir", type=click.Path(), help=HELP_FLOW_DIR)
@click.option("--db-filename", type=str, help=HELP_DB_FILENAME)
@click.option("--link-remote", help="Link workflow to n8n server (server name or URL)")
@click.option("--json", "output_json", is_flag=True, help=HELP_JSON)
@click.option("--no-emoji", is_flag=True, help=HELP_NO_EMOJI)
def add(
    workflow_file: str,
    data_dir: Optional[str],
    flow_dir: Optional[str],
    db_filename: Optional[str],
    link_remote: Optional[str],
    output_json: bool,
    no_emoji: bool,
) -> None:
    """➕ Register local workflow JSON file to database

    Adds a workflow from a local JSON file to the database. The workflow file
    should be in the flow directory. Optionally link to a remote n8n server.

    \b
    Examples:
      n8n-deploy wf add deAVBp391wvomsWY.json
      n8n-deploy wf add workflow.json --link-remote production
      n8n-deploy wf add workflow.json --link-remote https://n8n.example.com
    """
    if output_json:
        no_emoji = True

    try:
        # Use sentinel to distinguish "not provided" from explicit --flow-dir
        config = get_config(
            base_folder=data_dir,
            flow_folder=flow_dir if flow_dir is not None else NOT_PROVIDED,
            db_filename=db_filename,
        )
    except ValueError as e:
        cli_error(str(e), no_emoji)
        raise click.Abort()

    from .db import check_database_exists

    check_database_exists(config.database_path, output_json=output_json, no_emoji=no_emoji)

    from .db import check_database_exists

    check_database_exists(config.database_path, output_json=output_json, no_emoji=no_emoji)

    try:
        result = _read_workflow_file(config, workflow_file, output_json, no_emoji)
        if result is None:
            return  # JSON error already printed
        _, workflow_data = result

        workflow_id, workflow_name = _ensure_workflow_id(workflow_data, workflow_file, output_json, no_emoji)

        manager = WorkflowApi(config=config)
        manager.add_workflow(workflow_id, workflow_name, filename=workflow_file)

        _output_add_success(workflow_id, workflow_name, output_json, no_emoji)

        if link_remote:
            server_name, server_id = _resolve_server_for_linking(config, link_remote, output_json, no_emoji)
            _link_workflow_to_server(manager, workflow_id, server_id, server_name, output_json, no_emoji)

    except click.Abort:
        raise
    except Exception as e:
        if output_json:
            console.print(JSON.from_data({"success": False, "error": str(e)}))
        else:
            cli_error(f"Failed to add workflow: {e}", no_emoji)


@wf.command("list", cls=CustomCommand)
@click.option("--data-dir", type=click.Path(), help=cli_data_dir_help)
@click.option("--db-filename", type=str, help=HELP_DB_FILENAME)
@click.option("--json", "output_json", is_flag=True, help=HELP_JSON)
@click.option("--table", "output_table", is_flag=True, help=HELP_TABLE)
@click.option("--no-emoji", is_flag=True, help=HELP_NO_EMOJI)
def list(
    data_dir: Optional[str],
    db_filename: Optional[str],
    output_json: bool,
    output_table: bool,
    no_emoji: bool,
) -> None:
    """📋 List all workflows

    Displays all workflows from database with their metadata.
    """
    # JSON output implies no emoji
    if output_json:
        no_emoji = True

    try:
        config = get_config(base_folder=data_dir, db_filename=db_filename)
    except ValueError as e:
        console.print(f"[red]{e}[/red]")
        raise click.Abort()

    try:
        manager = WorkflowApi(config=config)
        workflows = manager.list_workflows()

        # Backupable status is shown in workflow metadata (file_exists field)
        # No filtering - all workflows are displayed with their backupable status

        if output_json:
            console.print(JSON.from_data(workflows))
        else:
            print_workflow_table(workflows, no_emoji)

    except Exception as e:
        error_msg = f"Failed to list workflows: {e}"
        if no_emoji:
            console.print(error_msg)
        else:
            console.print(f"[red]{error_msg}[/red]")
        raise click.Abort()


@wf.command(cls=CustomCommand)
@click.option(
    "--remote",
    metavar="N8N_SERVER_NAME|N8N_SERVER_URL",
    help="n8n server (name or URL) - uses linked API key if name provided",
)
@click.option("--skip-ssl-verify", is_flag=True, help="Skip SSL certificate verification for self-signed certificates")
@click.option("--data-dir", type=click.Path(), help=cli_data_dir_help)
@click.option("--flow-dir", type=click.Path(), help=HELP_FLOW_DIR)
@click.option("--db-filename", type=str, help=HELP_DB_FILENAME)
@click.option("--no-emoji", is_flag=True, help=HELP_NO_EMOJI)
@click.option("--yes", is_flag=True, help="Skip confirmation prompt")
@click.argument("workflow_id", metavar="WF_ID|WF_NAME|FILENAME")
def delete(
    workflow_id: str,
    remote: Optional[str],
    skip_ssl_verify: bool,
    data_dir: Optional[str],
    flow_dir: Optional[str],
    db_filename: Optional[str],
    no_emoji: bool,
    yes: bool,
) -> None:
    """🗑️ Delete workflow from n8n server and database

    Deletes a workflow using its n8n workflow ID (e.g., 'deAVBp391wvomsWY'),
    workflow name, or filename. The workflow is deleted from the remote server
    first, then removed from the local database. The JSON file is NOT deleted.

    Server Resolution Priority (lowest to highest):
    1. Workflow's linked server (set via 'wf add --link-remote')
    2. N8N_SERVER_URL environment variable
    3. --remote option (overrides all)

    \b
    Examples:
      n8n-deploy wf delete workflow-name              # Uses linked server
      n8n-deploy wf delete workflow-name --remote staging  # Override to staging
      n8n-deploy wf delete deAVBp391wvomsWY --yes    # Skip confirmation
      n8n-deploy wf delete my-workflow.json          # Delete by filename
    """
    try:
        # Use sentinel to distinguish "not provided" from explicit --flow-dir
        config = get_config(
            base_folder=data_dir,
            flow_folder=flow_dir if flow_dir is not None else NOT_PROVIDED,
            db_filename=db_filename,
        )
    except ValueError as e:
        console.print(f"[red]{e}[/red]")
        raise click.Abort()

    try:
        manager = WorkflowApi(config=config, skip_ssl_verify=skip_ssl_verify, remote=remote)

        # Get workflow info for confirmation (resolves ID/name/filename)
        try:
            workflow_info = manager.get_workflow_info(workflow_id)
            workflow_name = workflow_info.get("name", workflow_id)
            actual_id = workflow_info["wf"].id
        except Exception:
            workflow_name = workflow_id
            actual_id = workflow_id

        # Check if this is a draft workflow (local only, no server deletion needed)
        is_draft = actual_id.startswith("draft_")

        # Ask for confirmation unless --yes flag is provided
        if not yes:
            if is_draft:
                prompt_msg = f"Remove draft workflow '{workflow_name}' ({actual_id}) from database?"
            else:
                prompt_msg = f"Delete workflow '{workflow_name}' ({actual_id}) from server and database?"

            if no_emoji:
                confirmation = click.confirm(prompt_msg)
            else:
                confirmation = click.confirm(f"🗑️ {prompt_msg}")

            if not confirmation:
                if no_emoji:
                    console.print("Operation cancelled")
                else:
                    console.print("[yellow]Operation cancelled[/yellow]")
                return

        # Delete from server first (unless it's a draft)
        if not is_draft:
            success = manager.delete_n8n_workflow(actual_id)
            if not success:
                error_msg = f"Failed to delete workflow from server"
                if no_emoji:
                    console.print(error_msg)
                else:
                    console.print(f"[red]{error_msg}[/red]")
                raise click.Abort()

        # Remove from database
        manager.remove_workflow(actual_id)

        if is_draft:
            success_msg = f"Removed draft workflow '{workflow_name}' from database"
        else:
            success_msg = f"Deleted workflow '{workflow_name}' from server and database"

        if no_emoji:
            console.print(success_msg)
        else:
            console.print(f"[green]✓ {success_msg}[/green]")

    except click.Abort:
        raise
    except Exception as e:
        error_msg = f"Failed to delete workflow: {e}"
        if no_emoji:
            console.print(error_msg)
        else:
            console.print(f"[red]{error_msg}[/red]")
        raise click.Abort()


@wf.command(cls=CustomCommand)
@click.argument("query")
@click.option("--data-dir", type=click.Path(), help=cli_data_dir_help)
@click.option("--flow-dir", type=click.Path(), help=HELP_FLOW_DIR)
@click.option("--db-filename", type=str, help=HELP_DB_FILENAME)
@click.option("--json", "output_json", is_flag=True, help=HELP_JSON)
@click.option("--table", "output_table", is_flag=True, help=HELP_TABLE)
@click.option("--no-emoji", is_flag=True, help=HELP_NO_EMOJI)
def search(
    query: str,
    data_dir: Optional[str],
    flow_dir: Optional[str],
    db_filename: Optional[str],
    output_json: bool,
    output_table: bool,
    no_emoji: bool,
) -> None:
    """🔍 Search workflows by name or wf ID

    Searches both:
    - User-friendly names assigned in n8n-deploy (e.g., 'signup-flow')
    - n8n wf IDs (e.g., 'deAVBp391wvomsWY' or partial matches)

    Results are ordered by relevance: exact matches first, then partial matches.
    Use exact n8n wf IDs for direct operations like pull/push/delete.
    """
    # JSON output implies no emoji
    if output_json:
        no_emoji = True

    try:
        # Use sentinel to distinguish "not provided" from explicit --flow-dir
        config = get_config(
            base_folder=data_dir,
            flow_folder=flow_dir if flow_dir is not None else NOT_PROVIDED,
            db_filename=db_filename,
        )
    except ValueError as e:
        console.print(f"[red]{e}[/red]")
        raise click.Abort()

    try:
        manager = WorkflowApi(config=config)
        workflows = manager.search_workflows(query)

        if output_json:
            console.print(JSON.from_data(workflows))
        else:
            print_workflow_search_table(workflows, no_emoji, query)

    except Exception as e:
        error_msg = f"Failed to search workflows: {e}"
        if no_emoji:
            console.print(error_msg)
        else:
            console.print(f"[red]{error_msg}[/red]")
        raise click.Abort()


@wf.command(cls=CustomCommand)
@click.option("--data-dir", type=click.Path(), help=cli_data_dir_help)
@click.option("--flow-dir", type=click.Path(), help=HELP_FLOW_DIR)
@click.option("--db-filename", type=str, help=HELP_DB_FILENAME)
@click.option("--json", "output_json", is_flag=True, help=HELP_JSON)
@click.option("--table", "output_table", is_flag=True, help=HELP_TABLE)
@click.option("--no-emoji", is_flag=True, help=HELP_NO_EMOJI)
@click.argument("workflow_id", required=False, metavar="wf-id")
def stats(
    workflow_id: Optional[str],
    data_dir: Optional[str],
    flow_dir: Optional[str],
    db_filename: Optional[str],
    output_json: bool,
    output_table: bool,
    no_emoji: bool,
) -> None:
    """📊 Show wf statistics

    Shows overall wf statistics if no wf-id is provided,
    or detailed statistics for a specific wf if wf-id is given.

    The wf-id should be the actual n8n wf ID (e.g., 'deAVBp391wvomsWY'),
    not the user-friendly name assigned in n8n-deploy.
    """
    # JSON output implies no emoji
    if output_json:
        no_emoji = True

    try:
        # Use sentinel to distinguish "not provided" from explicit --flow-dir
        config = get_config(
            base_folder=data_dir,
            flow_folder=flow_dir if flow_dir is not None else NOT_PROVIDED,
            db_filename=db_filename,
        )
    except ValueError as e:
        console.print(f"[red]{e}[/red]")
        raise click.Abort()

    try:
        manager = WorkflowApi(config=config)
        stats_data = manager.get_workflow_stats(workflow_id)

        if output_json:
            console.print(JSON.from_data(stats_data))
        else:
            if workflow_id:
                # Individual wf stats
                table = Table()
                table.add_column("Property", style="cyan")
                table.add_column("Value", style="magenta")

                for key, value in stats_data.items():
                    table.add_row(key, str(value) if value is not None else "-")

                console.print(table)
            else:
                # Overall stats
                table = Table()
                table.add_column("Metric", style="cyan")
                table.add_column("Count", justify="right", style="magenta")

                table.add_row("Total Workflows", str(stats_data["total_workflows"]))
                table.add_row("Total Push Operations", str(stats_data["total_push_operations"]))
                table.add_row("Total Pull Operations", str(stats_data["total_pull_operations"]))

                console.print(table)

    except Exception as e:
        error_msg = f"Failed to get stats: {e}"
        if no_emoji:
            console.print(error_msg)
        else:
            console.print(f"[red]{error_msg}[/red]")
        raise click.Abort()


# Server operations
@wf.command(cls=CustomCommand)
@click.option(
    "--remote",
    metavar="N8N_SERVER_NAME|N8N_SERVER_URL",
    help="n8n server (name or URL) - uses linked API key if name provided",
)
@click.option("--skip-ssl-verify", is_flag=True, help="Skip SSL certificate verification for self-signed certificates")
@click.option("--data-dir", type=click.Path(), help=cli_data_dir_help)
@click.option("--flow-dir", type=click.Path(), help=HELP_FLOW_DIR)
@click.option("--db-filename", type=str, help=HELP_DB_FILENAME)
@click.option("--filename", metavar="FILENAME", help="Custom filename for new workflows (e.g., 'my-workflow.json')")
@click.option("--no-emoji", is_flag=True, help=HELP_NO_EMOJI)
@click.argument("workflow_id", metavar="WORKFLOW_ID|WORKFLOW_NAME")
def pull(
    workflow_id: str,
    remote: Optional[str],
    skip_ssl_verify: bool,
    data_dir: Optional[str],
    flow_dir: Optional[str],
    db_filename: Optional[str],
    filename: Optional[str],
    no_emoji: bool,
) -> None:
    """📥 Download wf from n8n server

    Downloads a wf using its n8n wf ID (e.g., 'deAVBp391wvomsWY') or workflow name.

    Server Resolution Priority (lowest to highest):
    1. Workflow's linked server (if workflow exists in database)
    2. N8N_SERVER_URL environment variable
    3. --remote option (overrides all)

    Use --remote to override with server name (e.g., 'production') or URL.
    If server name is used, the linked API key will be used automatically.

    For new workflows (not in database), use --filename to specify the local filename.
    If not provided, you will be prompted to enter one.

    Examples:
      n8n-deploy wf pull workflow-name              # Uses linked server
      n8n-deploy wf pull workflow-name --remote staging  # Override to staging
      n8n-deploy wf pull abc123 --filename my-workflow.json  # Custom filename
    """
    try:
        # Use sentinel to distinguish "not provided" from explicit --flow-dir
        config = get_config(
            base_folder=data_dir,
            flow_folder=flow_dir if flow_dir is not None else NOT_PROVIDED,
            db_filename=db_filename,
        )
    except ValueError as e:
        console.print(f"[red]{e}[/red]")
        raise click.Abort()

    # Check if database exists and is initialized
    from .db import check_database_exists

    check_database_exists(config.database_path, output_json=False, no_emoji=no_emoji)

    try:
        manager = WorkflowApi(config=config, skip_ssl_verify=skip_ssl_verify, remote=remote)

        # Check if workflow exists in database
        # If not, and no filename provided, prompt user
        target_filename = filename
        try:
            manager.get_workflow_info(workflow_id)
            # Workflow exists - filename will be retrieved from database
        except ValueError:
            # Workflow not in database - this is a new pull
            if not target_filename:
                # Prompt user for filename
                default_filename = f"{workflow_id}.json"
                if no_emoji:
                    console.print(f"New workflow detected. Enter filename (default: {default_filename}):")
                else:
                    console.print(f"📄 New workflow detected. Enter filename (default: [cyan]{default_filename}[/cyan]):")

                target_filename = click.prompt("Filename", default=default_filename, show_default=False)

                # Ensure .json extension
                if target_filename and not target_filename.endswith(".json"):
                    target_filename = f"{target_filename}.json"

        success = manager.pull_workflow(workflow_id, filename=target_filename)

        if success:
            success_msg = f"Pulled wf '{workflow_id}' from server"
            if no_emoji:
                console.print(success_msg)
            else:
                console.print(f"[green]✓ {success_msg}[/green]")
        else:
            error_msg = f"Failed to pull wf '{workflow_id}'"
            if no_emoji:
                console.print(error_msg)
            else:
                console.print(f"[red]{error_msg}[/red]")
            raise click.Abort()

    except click.Abort:
        # Re-raise Abort without additional message
        raise
    except Exception as e:
        error_msg = f"Failed to pull wf: {e}"
        if no_emoji:
            console.print(error_msg)
        else:
            console.print(f"[red]{error_msg}[/red]")
        raise click.Abort()


@wf.command(cls=CustomCommand)
@click.option(
    "--remote",
    metavar="N8N_SERVER_NAME|N8N_SERVER_URL",
    help="n8n server (name or URL) - uses linked API key if name provided",
)
@click.option("--skip-ssl-verify", is_flag=True, help="Skip SSL certificate verification for self-signed certificates")
@click.option("--data-dir", type=click.Path(), help=cli_data_dir_help)
@click.option("--flow-dir", type=click.Path(), help=HELP_FLOW_DIR)
@click.option("--db-filename", type=str, help=HELP_DB_FILENAME)
@click.option("--no-emoji", is_flag=True, help=HELP_NO_EMOJI)
# Script sync options
@click.option(
    "--scripts",
    type=click.Path(exists=True, file_okay=False, dir_okay=True),
    help="Local directory containing scripts (.js, .cjs, .py) to sync",
)
@click.option(
    "--scripts-base-path",
    type=str,
    default="/opt/n8n/scripts",
    show_default=True,
    envvar="N8N_SCRIPTS_BASE_PATH",
    help="Remote base path (workflow name appended as subfolder) [env: N8N_SCRIPTS_BASE_PATH]",
)
@click.option(
    "--scripts-host",
    type=str,
    envvar="N8N_SCRIPTS_HOST",
    help="Remote host for script sync [env: N8N_SCRIPTS_HOST]",
)
@click.option(
    "--scripts-user",
    type=str,
    envvar="N8N_SCRIPTS_USER",
    help="Remote username for script sync [env: N8N_SCRIPTS_USER]",
)
@click.option(
    "--scripts-port",
    type=int,
    default=22,
    show_default=True,
    envvar="N8N_SCRIPTS_PORT",
    help="Remote SSH port [env: N8N_SCRIPTS_PORT]",
)
@click.option(
    "--scripts-key",
    type=click.Path(exists=True),
    envvar="N8N_SCRIPTS_KEY",
    help="SSH key file for script sync [env: N8N_SCRIPTS_KEY]",
)
@click.option(
    "--scripts-all",
    is_flag=True,
    help="Sync all scripts (not just git-changed ones)",
)
@click.option(
    "--dry-run",
    is_flag=True,
    help="Preview mode: show what would be synced without transferring",
)
@click.argument("workflow_id", metavar="WORKFLOW_ID|WORKFLOW_NAME")
def push(
    workflow_id: str,
    remote: Optional[str],
    skip_ssl_verify: bool,
    data_dir: Optional[str],
    flow_dir: Optional[str],
    db_filename: Optional[str],
    no_emoji: bool,
    scripts: Optional[str],
    scripts_base_path: str,
    scripts_host: Optional[str],
    scripts_user: Optional[str],
    scripts_port: int,
    scripts_key: Optional[str],
    scripts_all: bool,
    dry_run: bool,
) -> None:
    """📤 Upload wf to n8n server

    Uploads a wf using its n8n wf ID (e.g., 'deAVBp391wvomsWY') or workflow name.

    Server Resolution Priority (lowest to highest):
    1. Workflow's linked server (set via 'wf add --link-remote')
    2. N8N_SERVER_URL environment variable
    3. --remote option (overrides all)

    Use --remote to override with server name (e.g., 'production') or URL.
    If server name is used, the linked API key will be used automatically.

    Script Sync (optional):
    Use --scripts to sync external scripts referenced by Execute Command nodes.
    Requires --scripts-host, --scripts-user, and either --scripts-key or password.
    Uses git to detect changed scripts (use --scripts-all to sync all).

    Examples:
      n8n-deploy wf push workflow-name              # Uses linked server
      n8n-deploy wf push workflow-name --remote staging  # Override to staging
      n8n-deploy wf push workflow-name --scripts ./scripts --scripts-host n8n.example.com --scripts-user deploy --scripts-key ~/.ssh/id_rsa
    """
    try:
        # Use sentinel to distinguish "not provided" from explicit --flow-dir
        config = get_config(
            base_folder=data_dir,
            flow_folder=flow_dir if flow_dir is not None else NOT_PROVIDED,
            db_filename=db_filename,
        )
    except ValueError as e:
        console.print(f"[red]{e}[/red]")
        raise click.Abort()

    try:
        manager = WorkflowApi(config=config, skip_ssl_verify=skip_ssl_verify, remote=remote)
        success = manager.push_workflow(workflow_id)

        if success:
            success_msg = f"Pushed wf '{workflow_id}' to server"
            if no_emoji:
                console.print(success_msg)
            else:
                console.print(f"[green]✓ {success_msg}[/green]")
        else:
            error_msg = f"Failed to push wf '{workflow_id}'"
            if no_emoji:
                console.print(error_msg)
            else:
                console.print(f"[red]{error_msg}[/red]")
            raise click.Abort()

        # Script sync (only if workflow push succeeded and --scripts provided)
        if scripts:
            _sync_scripts_for_workflow(
                config=config,
                workflow_id=workflow_id,
                scripts_dir=scripts,
                scripts_base_path=scripts_base_path,
                scripts_host=scripts_host,
                scripts_user=scripts_user,
                scripts_port=scripts_port,
                scripts_key=scripts_key,
                scripts_all=scripts_all,
                dry_run=dry_run,
                no_emoji=no_emoji,
            )

    except click.Abort:
        raise  # Re-raise without additional message
    except Exception as e:
        error_msg = f"Failed to push wf: {e}"
        if no_emoji:
            console.print(error_msg)
        else:
            console.print(f"[red]{error_msg}[/red]")
        raise click.Abort()


def _sync_scripts_for_workflow(
    config: AppConfig,
    workflow_id: str,
    scripts_dir: str,
    scripts_base_path: str,
    scripts_host: Optional[str],
    scripts_user: Optional[str],
    scripts_port: int,
    scripts_key: Optional[str],
    scripts_all: bool,
    dry_run: bool,
    no_emoji: bool,
) -> None:
    """Sync scripts for a workflow after push.

    Args:
        config: Application configuration
        workflow_id: Workflow ID or name
        scripts_dir: Local scripts directory
        scripts_base_path: Remote base path
        scripts_host: Remote host
        scripts_user: Remote username
        scripts_port: SSH port
        scripts_key: SSH key file path
        scripts_all: Sync all scripts (not just changed)
        dry_run: Dry run mode
        no_emoji: Disable emoji output
    """
    from ..workflow.script_sync import ScriptSyncConfig, ScriptSyncManager

    # Validate required options
    if not scripts_host:
        error_msg = "--scripts-host is required when using --scripts"
        if no_emoji:
            console.print(error_msg)
        else:
            console.print(f"[red]{error_msg}[/red]")
        raise click.Abort()

    if not scripts_user:
        error_msg = "--scripts-user is required when using --scripts"
        if no_emoji:
            console.print(error_msg)
        else:
            console.print(f"[red]{error_msg}[/red]")
        raise click.Abort()

    if not scripts_key:
        error_msg = "--scripts-key is required when using --scripts"
        if no_emoji:
            console.print(error_msg)
        else:
            console.print(f"[red]{error_msg}[/red]")
        raise click.Abort()

    # Load workflow data
    crud = WorkflowApi(config=config)
    info = crud.crud.get_workflow_info(workflow_id)
    if not info:
        error_msg = f"Workflow '{workflow_id}' not found in database"
        if no_emoji:
            console.print(error_msg)
        else:
            console.print(f"[red]{error_msg}[/red]")
        raise click.Abort()

    workflow_file = info["wf"].file
    if not workflow_file:
        error_msg = f"Workflow '{workflow_id}' has no associated file"
        if no_emoji:
            console.print(error_msg)
        else:
            console.print(f"[red]{error_msg}[/red]")
        raise click.Abort()

    workflow_path = config.workflows_path / workflow_file
    if not workflow_path.exists():
        error_msg = f"Workflow file not found: {workflow_path}"
        if no_emoji:
            console.print(error_msg)
        else:
            console.print(f"[red]{error_msg}[/red]")
        raise click.Abort()

    # Load workflow JSON
    with open(workflow_path, "r", encoding="utf-8") as f:
        workflow_data = json.load(f)

    workflow_name = workflow_data.get("name", workflow_id)
    # Sanitize for use as remote directory name
    sanitized_name = ScriptSyncManager.sanitize_workflow_name(workflow_name)

    # Create sync config
    sync_config = ScriptSyncConfig(
        scripts_dir=Path(scripts_dir),
        remote_base_path=scripts_base_path,
        workflow_name=sanitized_name,
        host=scripts_host,
        port=scripts_port,
        username=scripts_user,
        key_file=Path(scripts_key).expanduser() if scripts_key else None,
        changed_only=not scripts_all,
        dry_run=dry_run,
    )

    try:
        sync_manager = ScriptSyncManager(sync_config)
    except ValueError as e:
        error_msg = f"Script sync config error: {e}"
        if no_emoji:
            console.print(error_msg)
        else:
            console.print(f"[red]{error_msg}[/red]")
        raise click.Abort()

    # Perform sync
    if dry_run:
        console.print("\n[yellow]Script sync dry run:[/yellow]" if not no_emoji else "\nScript sync dry run:")

    result = sync_manager.sync_scripts(workflow_data)

    # Report results
    if result.success:
        if result.scripts_synced > 0:
            sync_msg = f"Synced {result.scripts_synced} script(s) to {scripts_host}"
            if no_emoji:
                console.print(sync_msg)
            else:
                console.print(f"[green]✓ {sync_msg}[/green]")

            for script_info in result.synced_files:
                console.print(f"  - {script_info}")

        if result.scripts_skipped > 0:
            skip_msg = f"Skipped {result.scripts_skipped} unchanged script(s)"
            if no_emoji:
                console.print(skip_msg)
            else:
                console.print(f"[dim]{skip_msg}[/dim]")

        for warning in result.warnings:
            if no_emoji:
                console.print(f"Warning: {warning}")
            else:
                console.print(f"[yellow]Warning: {warning}[/yellow]")
    else:
        for error in result.errors:
            if no_emoji:
                console.print(f"Script sync error: {error}")
            else:
                console.print(f"[red]Script sync error: {error}[/red]")
        raise click.Abort()


@wf.command("server", cls=CustomCommand)
@click.option("--remote", help=HELP_SERVER_URL)
@click.option("--skip-ssl-verify", is_flag=True, help="Skip SSL certificate verification for self-signed certificates")
@click.option("--data-dir", type=click.Path(), help=cli_data_dir_help)
@click.option("--flow-dir", type=click.Path(), help=HELP_FLOW_DIR)
@click.option("--db-filename", type=str, help=HELP_DB_FILENAME)
@click.option("--json", "output_json", is_flag=True, help=HELP_JSON)
@click.option("--table", "output_table", is_flag=True, help=HELP_TABLE)
@click.option("--no-emoji", is_flag=True, help=HELP_NO_EMOJI)
def list_server(
    remote: Optional[str],
    skip_ssl_verify: bool,
    data_dir: Optional[str],
    flow_dir: Optional[str],
    db_filename: Optional[str],
    output_json: bool,
    output_table: bool,
    no_emoji: bool,
) -> None:
    """🌐 List workflows from n8n server"""
    # JSON output implies no emoji
    if output_json:
        no_emoji = True

    try:
        # Use sentinel to distinguish "not provided" from explicit --flow-dir
        config = get_config(
            base_folder=data_dir,
            flow_folder=flow_dir if flow_dir is not None else NOT_PROVIDED,
            db_filename=db_filename,
        )
    except ValueError as e:
        console.print(f"[red]{e}[/red]")
        raise click.Abort()

    try:
        manager = WorkflowApi(config=config, skip_ssl_verify=skip_ssl_verify, remote=remote)
        workflows = manager.list_n8n_workflows()

        if output_json:
            console.print(JSON.from_data(workflows))
        else:
            if not workflows:
                msg = "No workflows found on server"
                if no_emoji:
                    console.print(msg)
                else:
                    console.print(f"[yellow]{msg}[/yellow]")
                return

            table = Table()
            table.add_column("ID", style="cyan", no_wrap=True)
            table.add_column("Name", style="magenta")
            table.add_column("Active", justify="center")
            table.add_column("Updated", justify="center")

            for wf in workflows:
                table.add_row(
                    wf.get("id", ""),
                    wf.get("name", ""),
                    "✓" if wf.get("active") else "✗",
                    str(wf.get("updatedAt", ""))[:10] if wf.get("updatedAt") else "-",
                )

            console.print(table)

    except Exception as e:
        error_msg = f"Failed to list server workflows: {e}"
        if no_emoji:
            console.print(error_msg)
        else:
            console.print(f"[red]{error_msg}[/red]")
        raise click.Abort()


@wf.command(cls=CustomCommand)
@click.argument("workflow_id")
@click.option("--flow-dir", type=click.Path(), help="Update workflow's flow directory")
@click.option("--server", "server_name", help="Link workflow to server (by name)")
@click.option("--scripts-path", help="Update scripts path for Execute Command nodes")
@click.option("--data-dir", type=click.Path(), help=cli_data_dir_help)
@click.option("--db-filename", type=str, help=HELP_DB_FILENAME)
@click.option("--json", "output_json", is_flag=True, help=HELP_JSON)
@click.option("--no-emoji", is_flag=True, help=HELP_NO_EMOJI)
def link(
    workflow_id: str,
    flow_dir: Optional[str],
    server_name: Optional[str],
    scripts_path: Optional[str],
    data_dir: Optional[str],
    db_filename: Optional[str],
    output_json: bool,
    no_emoji: bool,
) -> None:
    """🔗 Update workflow metadata (flow-dir, server, scripts-path)

    Updates stored metadata for a workflow without performing push/pull.
    Use this to configure where a workflow's files are located or which
    server it should sync with.

    \b
    Examples:
      n8n-deploy wf link my-workflow --flow-dir ./workflows
      n8n-deploy wf link my-workflow --server production
      n8n-deploy wf link my-workflow --scripts-path /opt/n8n/scripts
      n8n-deploy wf link my-workflow --flow-dir ./workflows --server production
    """
    if output_json:
        no_emoji = True

    try:
        config = get_config(
            base_folder=data_dir,
            flow_folder=NOT_PROVIDED,  # Not used for linking
            db_filename=db_filename,
        )
    except ValueError as e:
        cli_error(str(e), no_emoji)
        raise click.Abort()

    from .db import check_database_exists

    check_database_exists(config.database_path, output_json=output_json, no_emoji=no_emoji)

    try:
        manager = WorkflowApi(config=config)

        # Find workflow by ID or name
        workflow_obj = manager.db.get_workflow(workflow_id)
        if not workflow_obj:
            # Try searching by name
            workflows = manager.db.search_workflows(workflow_id)
            if len(workflows) == 1:
                workflow_obj = workflows[0]
            elif len(workflows) > 1:
                error_msg = f"Multiple workflows match '{workflow_id}'. Use the full workflow ID."
                if output_json:
                    console.print(JSON.from_data({"success": False, "error": error_msg}))
                else:
                    cli_error(error_msg, no_emoji)
                raise click.Abort()

        if not workflow_obj:
            error_msg = f"Workflow '{workflow_id}' not found in database"
            if output_json:
                console.print(JSON.from_data({"success": False, "error": error_msg}))
            else:
                cli_error(error_msg, no_emoji)
            raise click.Abort()

        updated_fields: List[str] = []

        # Update flow_dir
        if flow_dir is not None:
            resolved_path = str(Path(flow_dir).resolve())
            workflow_obj.file_folder = resolved_path
            updated_fields.append(f"flow-dir={resolved_path}")

        # Update server linkage
        if server_name is not None:
            server_name_resolved, server_id = _resolve_server_for_linking(config, server_name, output_json, no_emoji)
            workflow_obj.server_id = server_id
            updated_fields.append(f"server={server_name_resolved}")

        # Update scripts path
        if scripts_path is not None:
            workflow_obj.scripts_path = scripts_path
            updated_fields.append(f"scripts-path={scripts_path}")

        if not updated_fields:
            warning_msg = "No updates specified. Use --flow-dir, --server, or --scripts-path"
            if output_json:
                console.print(JSON.from_data({"success": False, "error": warning_msg}))
            else:
                if no_emoji:
                    console.print(warning_msg)
                else:
                    console.print(f"[yellow]{warning_msg}[/yellow]")
            return

        # Save updates
        manager.db.update_workflow(workflow_obj)

        # Output success
        result = {
            "success": True,
            "workflow_id": workflow_obj.id,
            "workflow_name": workflow_obj.name,
            "updated": updated_fields,
        }

        if output_json:
            console.print(JSON.from_data(result))
        elif no_emoji:
            console.print(f"Updated workflow '{workflow_obj.name}': {', '.join(updated_fields)}")
        else:
            console.print(f"✅ Updated workflow '{workflow_obj.name}': {', '.join(updated_fields)}")

    except click.Abort:
        raise
    except Exception as e:
        error_msg = f"Failed to update workflow: {e}"
        if output_json:
            console.print(JSON.from_data({"success": False, "error": error_msg}))
        else:
            cli_error(error_msg, no_emoji)
        raise click.Abort()
