#!/usr/bin/env python3
"""Update Jira issue fields based on YAML configuration.

Uses Typer for elegant CLI, loads credentials and configuration from files,
updates multiple custom fields with smart defaults and date calculations.
"""

import getpass
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Optional

import requests
import typer
import yaml
from prompt_toolkit import prompt
from prompt_toolkit.completion import FuzzyWordCompleter
from requests.auth import HTTPBasicAuth

from .jira_helper import load_jira_env
from .logging_helper import setup_logging

app = typer.Typer(
    add_completion=False,
    help="Update Jira issue fields based on configuration.",
    epilog="Credentials are loaded from ~/.config/jps-jira-utils/.jira.env",
)


def load_config(config_file: Optional[Path] = None) -> dict:
    """Load configuration from YAML file.

    Args:
        config_file: Optional path to configuration file.
                    Defaults to ~/.config/jps-jira-utils/config.yaml

    Returns:
        dict: Configuration data.

    Raises:
        Exit: If config file not found or invalid.
    """
    if config_file is None:
        config_path = Path.home() / ".config" / "jps-jira-utils" / "config.yaml"
    else:
        config_path = config_file.expanduser().resolve()

    if not config_path.exists():
        typer.echo(f"Error: Config file not found: {config_path}", err=True)
        typer.echo("Create it with project-specific settings.", err=True)
        raise typer.Exit(1)

    try:
        with open(config_path, "r") as f:
            config = yaml.safe_load(f)
        return config or {}
    except yaml.YAMLError as e:
        typer.echo(f"Error: Invalid YAML in config file: {e}", err=True)
        raise typer.Exit(1)


def calculate_next_friday(start_date: datetime, business_days: int) -> str:
    """Calculate the Friday that falls after N business days from start date.

    Args:
        start_date: Starting date.
        business_days: Number of business days to add.

    Returns:
        str: Date in YYYY-MM-DD format.
    """
    current = start_date
    days_added = 0

    while days_added < business_days:
        current += timedelta(days=1)
        # Monday=0, Sunday=6
        if current.weekday() < 5:  # Weekday
            days_added += 1

    # Now find the next Friday
    days_until_friday = (4 - current.weekday()) % 7
    if days_until_friday == 0 and current.weekday() != 4:
        days_until_friday = 7

    friday = current + timedelta(days=days_until_friday)
    return friday.strftime("%Y-%m-%d")


def add_business_days(start_date_str: str, business_days: int) -> str:
    """Add business days to a date.

    Args:
        start_date_str: Starting date in YYYY-MM-DD format.
        business_days: Number of business days to add.

    Returns:
        str: Resulting date in YYYY-MM-DD format.
    """
    current = datetime.strptime(start_date_str, "%Y-%m-%d")
    days_added = 0

    while days_added < business_days:
        current += timedelta(days=1)
        if current.weekday() < 5:  # Weekday
            days_added += 1

    return current.strftime("%Y-%m-%d")


def get_issue_details(
    session: requests.Session, base_url: str, issue_key: str, logger
) -> dict:
    """Get current issue details.

    Args:
        session: Authenticated requests session.
        base_url: Jira base URL.
        issue_key: Issue key.
        logger: Logger instance.

    Returns:
        dict: Issue details.

    Raises:
        Exit: If fetching issue fails.
    """
    url = f"{base_url}/rest/api/3/issue/{issue_key}"
    logger.info("Fetching issue details for %s", issue_key)

    resp = session.get(url)
    if not resp.ok:
        logger.error("Failed to fetch issue: %d %s", resp.status_code, resp.text)
        typer.echo(f"Error: Failed to fetch issue: {resp.status_code}", err=True)
        raise typer.Exit(1)

    return resp.json()


def prompt_for_software_components(
    available_components: list[str], logger
) -> list[str]:
    """Prompt user to select software components interactively.

    Args:
        available_components: List of available software component names.
        logger: Logger instance.

    Returns:
        List of selected component names.
    """
    selected_components = []
    
    while True:
        typer.echo("\nAvailable Software Components:")
        for idx, component in enumerate(available_components, 1):
            typer.echo(f"  {idx}. {component}")
        
        typer.echo("\nEnter number or start typing to filter (press Enter to select):")
        
        # Create a completer for fuzzy matching
        completer = FuzzyWordCompleter(available_components)
        
        try:
            user_input = prompt("> ", completer=completer).strip()
            
            if not user_input:
                typer.echo("No selection made.")
                break
            
            # Check if input is a number
            if user_input.isdigit():
                idx = int(user_input)
                if 1 <= idx <= len(available_components):
                    selected = available_components[idx - 1]
                    if selected not in selected_components:
                        selected_components.append(selected)
                        logger.info("Selected software component: %s", selected)
                        typer.secho(f"✓ Added: {selected}", fg=typer.colors.GREEN)
                    else:
                        typer.echo(f"Component '{selected}' already selected.")
                else:
                    typer.echo(f"Invalid number. Please enter 1-{len(available_components)}.")
                    continue
            else:
                # User typed a component name
                if user_input in available_components:
                    if user_input not in selected_components:
                        selected_components.append(user_input)
                        logger.info("Selected software component: %s", user_input)
                        typer.secho(f"✓ Added: {user_input}", fg=typer.colors.GREEN)
                    else:
                        typer.echo(f"Component '{user_input}' already selected.")
                else:
                    typer.echo(f"Component '{user_input}' not found in list.")
                    continue
            
            # Ask if user wants to add another
            if not typer.confirm("Add another component?", default=True):
                break
                
        except KeyboardInterrupt:
            typer.echo("\nSelection cancelled.")
            break
        except EOFError:
            typer.echo("\nSelection cancelled.")
            break
    
    return selected_components


def get_field_id_by_name(
    session: requests.Session, base_url: str, field_name: str, logger
) -> Optional[str]:
    """Get custom field ID by field name.

    Args:
        session: Authenticated requests session.
        base_url: Jira base URL.
        field_name: Name of the field to find.
        logger: Logger instance.

    Returns:
        Optional[str]: Field ID or None if not found.
    """
    url = f"{base_url}/rest/api/3/field"
    resp = session.get(url)

    if not resp.ok:
        logger.warning("Failed to fetch fields: %d", resp.status_code)
        return None

    fields = resp.json()
    for field in fields:
        if field.get("name", "").lower() == field_name.lower():
            logger.info("Found field '%s' with ID: %s", field_name, field["id"])
            return field["id"]

    logger.warning("Field '%s' not found", field_name)
    return None


def get_sprints(
    session: requests.Session, base_url: str, board_id: int, logger
) -> list[dict]:
    """Get all sprints for a board.

    Args:
        session: Authenticated requests session.
        base_url: Jira base URL.
        board_id: Board ID.
        logger: Logger instance.

    Returns:
        list: List of sprint objects.
    """
    url = f"{base_url}/rest/agile/1.0/board/{board_id}/sprint"
    params = {"maxResults": 100}
    logger.info("Fetching sprints for board %d", board_id)

    resp = session.get(url, params=params)
    if not resp.ok:
        logger.warning(
            "Failed to fetch sprints for board %d: %d %s (sprint field will not be updated)",
            board_id,
            resp.status_code,
            resp.text,
        )
        return []

    data = resp.json()
    return data.get("values", [])


def find_latest_sprint(sprints: list[dict], project_key: str, logger) -> Optional[dict]:
    """Find the most recent sprint that starts with the project key.

    Args:
        sprints: List of sprint objects.
        project_key: Project key to match.
        logger: Logger instance.

    Returns:
        Optional[dict]: Sprint object or None.
    """
    matching_sprints = [
        s for s in sprints if s.get("name", "").startswith(project_key)
    ]

    if not matching_sprints:
        logger.warning("No sprints found starting with '%s'", project_key)
        return None

    # Sort by ID (assuming higher ID = more recent)
    matching_sprints.sort(key=lambda x: x.get("id", 0), reverse=True)
    latest = matching_sprints[0]
    logger.info("Found latest sprint: %s (ID: %s)", latest.get("name"), latest.get("id"))
    return latest


def get_current_user_account_id(
    session: requests.Session, base_url: str, logger
) -> str:
    """Get current user's account ID.

    Args:
        session: Authenticated requests session.
        base_url: Jira base URL.
        logger: Logger instance.

    Returns:
        str: Account ID.

    Raises:
        Exit: If fetching user fails.
    """
    url = f"{base_url}/rest/api/3/myself"
    resp = session.get(url)

    if not resp.ok:
        logger.error("Failed to fetch user info: %d", resp.status_code)
        typer.echo(f"Error: Failed to fetch user info: {resp.status_code}", err=True)
        raise typer.Exit(1)

    user_info = resp.json()
    account_id = user_info.get("accountId")
    logger.info("Current user account ID: %s", account_id)
    return account_id


def update_issue_fields(
    session: requests.Session,
    base_url: str,
    issue_key: str,
    updates: dict,
    logger,
    field_names: dict[str, str] = None,
) -> None:
    """Update issue fields.

    Args:
        session: Authenticated requests session.
        base_url: Jira base URL.
        issue_key: Issue key.
        updates: Dictionary of field updates.
        logger: Logger instance.
        field_names: Optional mapping of field IDs to human-readable names.

    Raises:
        Exit: If update fails.
    """
    if not updates:
        logger.info("No fields to update")
        typer.echo("No fields need updating (all already populated).")
        return

    url = f"{base_url}/rest/api/3/issue/{issue_key}"

    logger.info("Updating issue %s with %d fields", issue_key, len(updates))
    logger.debug("Update payload: %s", updates)

    # Retry loop to handle multiple rounds of field errors
    max_retries = 3
    retry_count = 0
    current_updates = updates.copy()
    failed_fields = {}  # field_id -> error_msg
    field_names = field_names or {}
    
    while retry_count < max_retries:
        resp = session.put(url, json={"fields": current_updates})
        
        if resp.ok:
            # Success - display results
            logger.info("Successfully updated %d fields", len(current_updates))
            
            typer.echo("\n" + "="*70)
            typer.echo("UPDATE RESULTS")
            typer.echo("="*70)
            
            if current_updates:
                typer.secho("\n✓ Successfully Updated:", fg=typer.colors.GREEN, bold=True)
                for field_id, value in current_updates.items():
                    field_name = field_names.get(field_id, field_id)
                    value_str = str(value)[:50] + "..." if len(str(value)) > 50 else str(value)
                    typer.secho(f"  ✓ {field_name}: {value_str}", fg=typer.colors.GREEN)
            
            if failed_fields:
                typer.secho(f"\n✗ Could Not Update ({len(failed_fields)} field(s)):", fg=typer.colors.RED, bold=True)
                for field_id, error_msg in failed_fields.items():
                    field_name = field_names.get(field_id, field_id)
                    typer.secho(f"  ✗ {field_name}: {error_msg}", fg=typer.colors.RED)
            
            typer.echo("="*70 + "\n")
            return
        
        # Check if it's a field-specific error
        try:
            error_data = resp.json()
            field_errors = error_data.get("errors", {})
            
            if field_errors and resp.status_code == 400:
                # Log and track field errors
                if retry_count == 0:
                    logger.warning("Some fields cannot be updated on this issue:")
                    
                for field_id, error_msg in field_errors.items():
                    logger.warning("  - %s: %s", field_id, error_msg)
                    failed_fields[field_id] = error_msg
                
                # Remove failed fields and retry
                current_updates = {k: v for k, v in current_updates.items() if k not in field_errors}
                
                if not current_updates:
                    logger.error("All fields failed validation")
                    typer.echo("\n" + "="*70)
                    typer.echo("UPDATE RESULTS")
                    typer.echo("="*70)
                    typer.secho(f"\n✗ Could Not Update ({len(failed_fields)} field(s)):", fg=typer.colors.RED, bold=True)
                    for field_id, error_msg in failed_fields.items():
                        field_name = field_names.get(field_id, field_id)
                        typer.secho(f"  ✗ {field_name}: {error_msg}", fg=typer.colors.RED)
                    typer.echo("="*70 + "\n")
                    typer.echo("Error: All fields are incompatible with this issue", err=True)
                    raise typer.Exit(1)
                
                logger.info("Retrying with %d remaining fields (removed %d)", len(current_updates), len(field_errors))
                if retry_count == 0:
                    typer.echo(f"\nSkipping {len(field_errors)} incompatible field(s), retrying with remaining fields...")
                    
                retry_count += 1
                continue
            else:
                # Non-field error, fail normally
                logger.error("Failed to update issue: %d %s", resp.status_code, resp.text)
                typer.echo(f"Error: Failed to update issue: {resp.status_code}", err=True)
                typer.echo(resp.text, err=True)
                raise typer.Exit(1)
        except ValueError:
            # Response is not JSON
            logger.error("Failed to update issue: %d %s", resp.status_code, resp.text)
            typer.echo(f"Error: Failed to update issue: {resp.status_code}", err=True)
            typer.echo(resp.text, err=True)
            raise typer.Exit(1)
    
    # Max retries exceeded
    logger.error("Max retries exceeded, some fields still failing")
    typer.echo("Error: Unable to update issue after multiple retries", err=True)
    raise typer.Exit(1)

    logger.info("Successfully updated issue %s", issue_key)


@app.command()
def main(
    issue_key: str = typer.Argument(..., help="Jira issue key, e.g. COMPBIO-1234"),
    config_file: Optional[Path] = typer.Option(
        None,
        "--config-file",
        help="Path to YAML config file (default: ~/.config/jps-jira-utils/config.yaml)",
    ),
    jira_config_file: Optional[Path] = typer.Option(
        None,
        "--jira-config",
        help="Path to Jira credentials file (default: ~/.config/jps-jira-utils/.jira.env)",
    ),
    board_id: Optional[int] = typer.Option(
        None, "--board-id", help="Jira board ID for sprint lookup"
    ),
    dry_run: bool = typer.Option(
        False, "--dry-run", help="Show what would be updated without making changes"
    ),
    logfile: Optional[Path] = typer.Option(
        None,
        "--logfile",
        help="Custom log file path",
    ),
) -> None:
    """Update Jira issue fields based on YAML configuration.

    Only updates fields that are currently empty/not set.

    Args:
        issue_key: The Jira issue key.
        config_file: Optional path to YAML configuration file.
        jira_config_file: Optional path to Jira credentials file.
        board_id: Optional board ID for sprint lookup.
        dry_run: If True, show changes without applying.
        logfile: Optional custom log file path.

    Raises:
        Exit: On error.
    """
    # Setup logging
    timestamp = datetime.now().strftime("%Y-%m-%d-%H%M%S")
    default_log_dir = Path("/tmp") / getpass.getuser() / "update_issue.py" / timestamp
    log_file = logfile or (default_log_dir / "update_issue.py.log")
    log_file = log_file.expanduser().resolve()

    logger = setup_logging(log_file)
    logger.info("=== update_issue.py started ===")
    logger.info("Issue key: %s", issue_key)
    logger.info("Dry run: %s", dry_run)

    # Load configurations
    config = load_config(config_file)
    logger.info("Loaded configuration")

    base_url, email, token = load_jira_env(jira_config_file)
    auth = HTTPBasicAuth(email, token)
    session = requests.Session()
    session.auth = auth
    session.headers.update({"Accept": "application/json", "Content-Type": "application/json"})

    # Get current issue details
    issue = get_issue_details(session, base_url, issue_key, logger)
    fields = issue.get("fields", {})

    # Get current user account ID
    current_user_account_id = get_current_user_account_id(session, base_url, logger)

    # Prepare updates dictionary
    updates = {}

    # Get custom field IDs
    software_component_field = get_field_id_by_name(
        session, base_url, "Software Component", logger
    )
    program_field = get_field_id_by_name(session, base_url, "PROGRAM", logger)
    ticket_type_field = get_field_id_by_name(session, base_url, "TICKET TYPE", logger)
    project_label_field = get_field_id_by_name(session, base_url, "Project Label", logger)
    team_field = get_field_id_by_name(session, base_url, "Team", logger)
    dev_complete_field = get_field_id_by_name(
        session, base_url, "Dev Complete Due Date", logger
    )
    qa_complete_field = get_field_id_by_name(
        session, base_url, "QA Complete Due Date", logger
    )
    uat_start_field = get_field_id_by_name(session, base_url, "UAT Start Date", logger)

    # Software Component - interactive selection from config list
    if software_component_field and not fields.get(software_component_field):
        software_components_list = config.get("software_components")
        
        if software_components_list and isinstance(software_components_list, list):
            # Interactive selection mode
            logger.info("Prompting user for software component selection")
            typer.echo("\n" + "="*70)
            typer.echo("SELECT SOFTWARE COMPONENTS")
            typer.echo("="*70)
            
            selected = prompt_for_software_components(software_components_list, logger)
            
            if selected:
                # Software Component field expects an array of strings
                # Send all selected components
                updates[software_component_field] = selected
                logger.info("Will update Software Component with %d selection(s): %s", len(selected), ", ".join(selected))

    # Sprint - find latest sprint with project key
    project_key = config.get("project_key")
    board_id_from_config = board_id or config.get("board_id")

    if board_id_from_config and project_key:
        current_sprint = fields.get("sprint")
        if not current_sprint:
            sprints = get_sprints(session, base_url, board_id_from_config, logger)
            latest_sprint = find_latest_sprint(sprints, project_key, logger)
            if latest_sprint:
                updates["sprint"] = latest_sprint["id"]
                logger.info("Will update Sprint: %s", latest_sprint.get("name"))

    # PROGRAM
    if program_field and not fields.get(program_field):
        program = config.get("program")
        if program:
            updates[program_field] = {"value": program}
            logger.info("Will update PROGRAM: %s", program)

    # TICKET TYPE
    if ticket_type_field and not fields.get(ticket_type_field):
        ticket_type = config.get("ticket_type")
        if ticket_type:
            updates[ticket_type_field] = {"value": ticket_type}
            logger.info("Will update TICKET TYPE: %s", ticket_type)

    # Project Label
    if project_label_field and not fields.get(project_label_field):
        project_label = config.get("project_label")
        if project_label:
            updates[project_label_field] = [project_label]
            logger.info("Will update Project Label: %s", project_label)

    # Team
    if team_field and not fields.get(team_field):
        team = config.get("team")
        if team:
            updates[team_field] = {"value": team}
            logger.info("Will update Team: %s", team)

    # Due Date
    if not fields.get("duedate"):
        due_date_days = config.get("default_due_date_business_days", 10)
        due_date = calculate_next_friday(datetime.now(), due_date_days)
        updates["duedate"] = due_date
        logger.info("Will update Due Date: %s (%d business days)", due_date, due_date_days)

    # Dev Complete Due Date
    dev_complete_date = None
    if dev_complete_field:
        if not fields.get(dev_complete_field):
            dev_complete_days = config.get("default_dev_complete_due_date_business_days", 5)
            dev_complete_date = calculate_next_friday(datetime.now(), dev_complete_days)
            updates[dev_complete_field] = dev_complete_date
            logger.info(
                "Will update Dev Complete Due Date: %s (%d business days)",
                dev_complete_date,
                dev_complete_days,
            )
        else:
            dev_complete_date = fields.get(dev_complete_field)

    # QA Complete Due Date
    qa_complete_date = None
    if qa_complete_field:
        if not fields.get(qa_complete_field):
            # Use the newly calculated or existing dev_complete_date
            if dev_complete_date:
                qa_complete_days = config.get("default_qa_complete_due_date_business_days", 2)
                qa_complete_date = add_business_days(dev_complete_date, qa_complete_days)
                updates[qa_complete_field] = qa_complete_date
                logger.info(
                    "Will update QA Complete Due Date: %s (%d business days after Dev Complete)",
                    qa_complete_date,
                    qa_complete_days,
                )
            else:
                logger.warning("Cannot calculate QA Complete Date without Dev Complete Date")
        else:
            qa_complete_date = fields.get(qa_complete_field)

    # UAT Start Date
    if uat_start_field and not fields.get(uat_start_field):
        # Use the newly calculated or existing qa_complete_date
        if qa_complete_date:
            uat_start_days = config.get("default_uat_start_date_business_days", 1)
            uat_start_date = add_business_days(qa_complete_date, uat_start_days)
            updates[uat_start_field] = uat_start_date
            logger.info(
                "Will update UAT Start Date: %s (%d business days after QA Complete)",
                uat_start_date,
                uat_start_days,
            )
        elif qa_complete_field:
            # Only warn if QA Complete field exists but has no value
            logger.warning("Cannot calculate UAT Start Date without QA Complete Date")

    # Assignee
    current_assignee = fields.get("assignee")
    if not current_assignee or not current_assignee.get("accountId"):
        updates["assignee"] = {"accountId": current_user_account_id}
        logger.info("Will update Assignee to current user")

    # Build field names mapping for display
    field_names = {
        "duedate": "Due Date",
        "assignee": "Assignee",
    }
    if software_component_field:
        field_names[software_component_field] = "Software Component"
    if program_field:
        field_names[program_field] = "PROGRAM"
    if ticket_type_field:
        field_names[ticket_type_field] = "TICKET TYPE"
    if project_label_field:
        field_names[project_label_field] = "Project Label"
    if team_field:
        field_names[team_field] = "Team"
    if dev_complete_field:
        field_names[dev_complete_field] = "Dev Complete Due Date"
    if qa_complete_field:
        field_names[qa_complete_field] = "QA Complete Due Date"
    if uat_start_field:
        field_names[uat_start_field] = "UAT Start Date"
    if "sprint" in updates:
        field_names["sprint"] = "Sprint"

    # Display what will be updated
    if updates:
        typer.echo("\n" + "=" * 70)
        typer.echo(f"UPDATES FOR {issue_key}")
        typer.echo("=" * 70)
        for field, value in updates.items():
            typer.echo(f"{field}: {value}")
        typer.echo("=" * 70)

        if dry_run:
            typer.echo("\nDRY RUN - No changes made.")
            logger.info("Dry run completed, no changes made")
        else:
            if typer.confirm("Apply these updates?", default=True):
                update_issue_fields(session, base_url, issue_key, updates, logger, field_names)
                typer.echo(f"\n\u2713 Successfully processed {issue_key}")
                typer.echo(f"URL: {base_url}/browse/{issue_key}")
            else:
                logger.info("User cancelled update")
                typer.echo("Cancelled.")
    else:
        typer.echo(f"All fields for {issue_key} are already populated. No updates needed.")

    typer.echo(f"Log file: {log_file}")


if __name__ == "__main__":
    app()
