#!/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 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 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.error("Failed to fetch sprints: %d %s", 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,
) -> 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.

    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}"
    payload = {"fields": updates}

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

    resp = session.put(url, json=payload)
    if not resp.ok:
        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)

    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 - from config
    if software_component_field and not fields.get(software_component_field):
        software_component = config.get("software_component")
        if software_component:
            updates[software_component_field] = {"value": software_component}
            logger.info("Will update Software Component: %s", software_component)

    # 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] = {"value": 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
    if dev_complete_field and 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) if dev_complete_field else None

    # QA Complete Due Date
    if qa_complete_field and not fields.get(qa_complete_field):
        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")

    # UAT Start Date
    if uat_start_field and not fields.get(uat_start_field):
        qa_complete_date_value = (
            updates.get(uat_start_field) or fields.get(qa_complete_field)
            if qa_complete_field
            else None
        )
        if qa_complete_date_value:
            uat_start_days = config.get("default_uat_start_date_business_days", 1)
            uat_start_date = add_business_days(qa_complete_date_value, 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,
            )
        else:
            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")

    # 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)
                typer.echo(f"\n✓ Successfully updated {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()
