"""Objective management tools.

Tools for retrieving and managing WorkBoard objectives (goals) and their key results.
WorkBoard uses "goal" in its API, but these tools expose OKR terminology.
"""

import asyncio
import contextlib
import logging
from datetime import datetime, timezone
from typing import Any

from ..client import get_client
from ..errors import UserError
from ..models import (
    CreateObjectiveInput,
    UpdateKeyResultInput,
    validate_metric_id,
    validate_objective_id,
    validate_user_id,
)

logger = logging.getLogger(__name__)



def _format_date(timestamp: str | int | None) -> str:
    """Convert Unix timestamp to YYYY-MM-DD."""
    if timestamp is None:
        return ""
    try:
        ts = int(timestamp)
    except (ValueError, TypeError):
        return ""
    if ts <= 0:
        return ""
    return datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d")


def _format_metric(
    metric: dict[str, Any],
    target_date_override: str | None = None,
) -> dict[str, Any]:
    """Format a key result metric for human-readable output.

    Args:
        metric: Raw metric dict from WorkBoard API
        target_date_override: Real target date (YYYY-MM-DD) to use instead of the
            value embedded in the nested metric object, which is often absent when
            metrics are returned as part of a goal response.
    """
    unit = metric.get("metric_unit", {})
    unit_name = unit.get("name", "") if isinstance(unit, dict) else ""

    try:
        target = float(metric.get("metric_target") or 0)
    except (ValueError, TypeError):
        target = 0
    try:
        achieved = float(metric.get("metric_achieve_target") or 0)
    except (ValueError, TypeError):
        achieved = 0

    match unit_name:
        case "Currency":
            progress_str = f"${int(achieved):,} of ${int(target):,}"
        case "Number":
            progress_str = f"{int(achieved)} of {int(target)}"
        case _:
            progress_str = f"{int(achieved)}% of {int(target)}%"

    formatted_metric: dict[str, Any] = {
        "metric_id": int(metric.get("metric_id", 0)),
        "name": metric.get("metric_name", ""),
        "progress": progress_str,
        "target_date": target_date_override or _format_date(metric.get("target_date")),
    }

    raw_last_update = metric.get("metric_last_update")
    try:
        last_update_ts = int(raw_last_update or 0)
    except (ValueError, TypeError):
        last_update_ts = 0

    formatted_metric["last_updated"] = (
        _format_date(last_update_ts) if last_update_ts > 0 else None
    )

    return formatted_metric


def _format_goal(
    goal: dict[str, Any],
    metric_target_dates: dict[int, str] | None = None,
) -> dict[str, Any]:
    """Format an objective for human-readable output.

    Args:
        goal: Raw goal dict from WorkBoard API
        metric_target_dates: Optional {metric_id: "YYYY-MM-DD"} lookup built from
            the /metric endpoint. When provided, used to populate target_date on
            nested key results, which the goal response leaves empty.
    """
    try:
        progress = int(float(goal.get("goal_progress") or 0))
    except (ValueError, TypeError):
        progress = 0

    formatted_goal: dict[str, Any] = {
        "objective_id": goal.get("goal_id"),
        "name": goal.get("goal_name", ""),
        "progress": f"{progress}%",
    }

    owner = goal.get("goal_owner_full_name")
    if owner:
        formatted_goal["owner"] = owner

    owner_id = goal.get("goal_owner")
    if owner_id:
        with contextlib.suppress(ValueError, TypeError):
            formatted_goal["owner_id"] = int(owner_id)

    team = goal.get("goal_team_name")
    if team:
        formatted_goal["team"] = team

    start = _format_date(goal.get("goal_start_date"))
    target = _format_date(goal.get("goal_target_date"))
    if start and target:
        formatted_goal["dates"] = f"{start} to {target}"

    metrics = goal.get("goal_metrics", [])
    if metrics:
        formatted_goal["key_results"] = [
            _format_metric(
                m,
                target_date_override=(
                    metric_target_dates.get(int(m.get("metric_id", 0)))
                    if metric_target_dates
                    else None
                ),
            )
            for m in metrics
        ]

    return formatted_goal


def _extract_goals_from_response(response: dict[str, Any]) -> tuple[list[dict[str, Any]], int]:
    """Extract goals list from the WorkBoard API response.

    The API returns goals as a dict with numeric string keys under
    data.user.goal, e.g. {"0": {goal}, "1": {goal}, "goals_count": 15}.
    This helper normalizes that into a list.

    Returns:
        Tuple of (goals list, total goal count)
    """
    response_data = response.get("data", {})
    if not isinstance(response_data, dict):
        return [], 0

    goal_count: int = response_data.get("goal_count", 0)
    user_data = response_data.get("user", {})
    if not isinstance(user_data, dict):
        return [], goal_count

    goals_obj = user_data.get("goal", {})

    match goals_obj:
        case dict():
            goals = [
                v for k, v in goals_obj.items()
                if k.isdigit() and isinstance(v, dict)
            ]
        case list():
            goals = goals_obj
        case _:
            goals = []

    return goals, goal_count


def _current_year_start() -> int:
    """Get Unix timestamp for Jan 1 of the current year."""
    now = datetime.now(timezone.utc)
    return int(datetime(now.year, 1, 1, tzinfo=timezone.utc).timestamp())



async def get_objectives(
    user_id: int,
) -> dict[str, Any]:
    """Get objectives associated with a WorkBoard user.

    Note: The WorkBoard API caps this endpoint at 15 results and returns
    objectives the user is associated with, not necessarily ones they own.
    For owned objectives, use get_my_objectives() with specific IDs.

    Args:
        user_id: User ID (positive integer)

    Returns:
        Dictionary containing list of objectives
    """
    user_id = validate_user_id(user_id)
    client = get_client()

    response, metric_target_dates = await asyncio.gather(
        client.get(f"/user/{user_id}/goal"),
        _fetch_target_date_map(client),
    )

    goals, goal_count = _extract_goals_from_response(response)
    result: dict[str, Any] = {
        "objectives": [_format_goal(g, metric_target_dates) for g in goals]
    }
    if goal_count > len(goals):
        result["warning"] = (
            f"Showing {len(goals)} of {goal_count} objectives (API cap). "
            f"Some objectives may be missing."
        )
    return result


async def get_objective_details(
    user_id: int,
    objective_id: int,
) -> dict[str, Any]:
    """Get details for a specific objective including key results.

    Args:
        user_id: User ID (positive integer)
        objective_id: Objective ID (positive integer)

    Returns:
        Objective details dictionary with key results
    """
    user_id = validate_user_id(user_id)
    objective_id = validate_objective_id(objective_id)
    client = get_client()

    response, metric_target_dates = await asyncio.gather(
        client.get(f"/user/{user_id}/goal/{objective_id}"),
        _fetch_target_date_map(client),
    )

    goal = response.get("data", {}).get("user", {}).get("goal", {})
    if isinstance(goal, dict) and goal:
        return {"objective": _format_goal(goal, metric_target_dates)}

    return {"objective": response}


async def _get_objective_ids_from_metrics(
    client: Any,
) -> tuple[list[int], dict[int, str]]:
    """Discover objective IDs and build a target-date lookup from the /metric endpoint.

    The /metric endpoint returns all key results the user has access to. Each metric
    contains a goal_id linking it to its parent objective and a real target_date that
    is absent from nested metric objects within goal responses.

    Returns:
        Tuple of:
        - Sorted list of unique objective IDs from current-year metrics
        - Dict mapping metric_id → "YYYY-MM-DD" target date for all fetched metrics
    """
    response = await client.get("/metric")

    data = response.get("data", {})
    all_metrics = data.get("metric", []) if isinstance(data, dict) else []

    target_date_map: dict[int, str] = {}
    for m in all_metrics:
        mid = m.get("metric_id")
        if mid is not None:
            try:
                target_date_map[int(mid)] = _format_date(m.get("target_date"))
            except (ValueError, TypeError):
                continue

    year_start = _current_year_start()
    current_year_metrics = [
        m for m in all_metrics
        if int(m.get("target_date") or 0) >= year_start
    ]

    goal_ids: set[int] = set()
    for m in current_year_metrics:
        gid = m.get("metric_goal_id")
        if gid is not None:
            try:
                goal_ids.add(int(gid))
            except (ValueError, TypeError):
                continue

    return sorted(goal_ids), target_date_map


async def _fetch_target_date_map(client: Any) -> dict[int, str]:
    """Fetch the /metric endpoint and return a {metric_id: target_date} lookup.

    Used to enrich nested key results within objectives. Fails gracefully —
    callers receive an empty dict when the fetch fails, which means nested KRs
    will have empty target_date strings rather than raising an error.
    """
    try:
        response = await client.get("/metric")
        data = response.get("data", {})
        metrics = data.get("metric", []) if isinstance(data, dict) else []
        return {
            int(m["metric_id"]): _format_date(m.get("target_date"))
            for m in metrics
            if m.get("metric_id") is not None
        }
    except Exception:
        logger.warning("Could not fetch metric target dates for enrichment")
        return {}


async def get_my_objectives(
    objective_ids: list[int] | None = None,
) -> dict[str, Any]:
    """Get the current user's objectives with key results.

    When objective IDs are provided, fetches each individually (explicit override).
    When no IDs are provided, auto-discovers objectives by fetching all metrics
    and extracting parent goal IDs, then fetching each objective individually.

    Args:
        objective_ids: Optional list of objective IDs to fetch. If not provided,
                       objectives are auto-discovered from the user's key results.

    Returns:
        Dictionary with objectives list
    """
    client = get_client()

    user_response = await client.get("/user")
    try:
        user_id = int(user_response["data"]["user"]["user_id"])
    except (KeyError, TypeError, ValueError):
        return {"error": "Could not determine current user ID"}

    metric_target_dates: dict[int, str] = {}
    if objective_ids is None:
        objective_ids, metric_target_dates = await _get_objective_ids_from_metrics(client)
        if not objective_ids:
            return {
                "objectives": [],
                "message": (
                    "No objectives found. No current-year key results were found "
                    "that link to objectives. You can provide objective IDs explicitly."
                ),
            }
    else:
        metric_target_dates = await _fetch_target_date_map(client)

    objectives: list[dict[str, Any]] = []
    skipped: list[dict[str, Any]] = []
    for oid in objective_ids:
        validated_oid = validate_objective_id(oid)
        try:
            detail = await client.get(f"/user/{user_id}/goal/{validated_oid}")
            goal = detail.get("data", {}).get("user", {}).get("goal", {})
            if isinstance(goal, dict) and goal:
                objectives.append(_format_goal(goal, metric_target_dates))
            else:
                objectives.append(detail)
        except UserError:
            skipped.append({
                "objective_id": validated_oid,
                "error": "Not accessible (may be archived or from a prior year)",
            })

    result: dict[str, Any] = {"objectives": objectives}
    if skipped:
        result["skipped"] = skipped
    return result


async def get_my_key_results(
    include_prior_years: bool = False,
) -> dict[str, Any]:
    """List all key results (metrics) the current user owns or has access to.

    By default, only returns key results with a target date in the current
    calendar year or later. Set include_prior_years=True for all key results.

    Args:
        include_prior_years: If True, include key results from prior years.
                             Defaults to False (current year only).

    Returns:
        Dictionary containing list of key results with their IDs, names,
        current values, and targets
    """
    client = get_client()

    response = await client.get("/metric")

    data = response.get("data", {})
    metrics = data.get("metric", []) if isinstance(data, dict) else []

    if not include_prior_years:
        year_start = _current_year_start()
        metrics = [
            m for m in metrics
            if int(m.get("target_date") or 0) >= year_start
        ]

    formatted = [_format_metric(m) for m in metrics]

    return {"key_results": formatted}


async def get_user_key_results(
    user_id: int,
    include_prior_years: bool = False,
) -> dict[str, Any]:
    """List key results (metrics) accessible to a specific WorkBoard user.

    Fetches metrics via /user/{user_id}/metric. The response may include
    metrics the user owns or is associated with, depending on WorkBoard
    access rules.

    By default, only returns key results with a target date in the current
    calendar year or later. Set include_prior_years=True for all key results.

    Args:
        user_id: User ID (positive integer)
        include_prior_years: If True, include key results from prior years.
                             Defaults to False (current year only).

    Returns:
        Dictionary containing list of key results with their IDs, names,
        current values, and targets
    """
    user_id = validate_user_id(user_id)
    client = get_client()

    response = await client.get(f"/user/{user_id}/metric")

    data = response.get("data", {})

    if isinstance(data, dict):
        metrics = (
            data.get("metric")
            or data.get("user", {}).get("metric")
            or []
        )
    else:
        metrics = []

    if not isinstance(metrics, list):
        metrics = []

    if not include_prior_years:
        year_start = _current_year_start()
        metrics = [
            m for m in metrics
            if int(m.get("target_date") or 0) >= year_start
        ]

    formatted = [_format_metric(m) for m in metrics]

    return {"key_results": formatted}


async def update_key_result(
    metric_id: int,
    value: str,
    comment: str | None = None,
) -> dict[str, Any]:
    """Update progress on a key result (metric).

    Includes input validation, read-back-before-write guard to warn on
    significant decreases, and audit logging.

    Args:
        metric_id: Metric ID (positive integer)
        value: The new progress value (e.g. "75")
        comment: Optional check-in comment

    Returns:
        Updated metric details (includes warning if value was decreased)
    """
    metric_id = validate_metric_id(metric_id)

    validated = UpdateKeyResultInput(value=value, comment=comment)

    client = get_client()

    current_value: float | None = None
    metric_name: str = f"metric/{metric_id}"
    try:
        current_response = await client.get("/metric")
        current_metrics = current_response.get("data", {}).get("metric", [])
        for m in current_metrics:
            if int(m.get("metric_id", 0)) == metric_id:
                current_value = float(m.get("metric_achieve_target") or 0)
                metric_name = m.get("metric_name", metric_name)
                break
    except Exception:
        logger.warning("Could not read current value for metric %d", metric_id)

    new_value = float(validated.value)

    warning: str | None = None
    if current_value is not None and new_value < current_value:
        warning = (
            f"Value decreased from {current_value} to {new_value} "
            f"for '{metric_name}' (metric_id={metric_id})"
        )
        logger.warning("AUDIT: Key result decrease — %s", warning)

    payload: dict[str, str] = {"metric_data": validated.value}
    if validated.comment is not None:
        payload["metric_comment"] = validated.comment

    response = await client.put(f"/metric/{metric_id}", json_data=payload)

    logger.info(
        "AUDIT: Key result updated — metric_id=%d, new_value=%s, comment=%s",
        metric_id,
        validated.value,
        validated.comment or "(none)",
    )

    result: dict[str, Any] = {"key_result": response}
    if warning:
        result["warning"] = warning
    return result


async def create_objective(
    name: str,
    owner: str,
    start_date: str,
    target_date: str,
    narrative: str | None = None,
    goal_type: str = "1",
    permission: str = "internal,team",
    key_results: list[dict[str, str]] | None = None,
) -> dict[str, Any]:
    """Create a new objective with optional key results (requires Data-Admin token).

    Args:
        name: Objective name
        owner: Owner's email address or user ID
        start_date: Start date (YYYY-MM-DD format)
        target_date: Target completion date (YYYY-MM-DD format)
        narrative: Optional description/narrative for the objective
        goal_type: "1" for Team objective, "2" for Personal objective
        permission: Visibility setting (default "internal,team")
        key_results: Optional list of key result dicts, each with keys like
                     "metric_name", "metric_start", "metric_target", "metric_type"

    Returns:
        Created objective details
    """
    validated = CreateObjectiveInput(
        name=name,
        owner=owner,
        start_date=start_date,
        target_date=target_date,
        narrative=narrative,
        goal_type=goal_type,
        permission=permission,
    )

    goal: dict[str, Any] = {
        "goal_name": validated.name,
        "goal_owner": validated.owner,
        "goal_start_date": validated.start_date,
        "goal_target_date": validated.target_date,
        "goal_type": validated.goal_type,
        "goal_permission": validated.permission,
    }

    if validated.narrative is not None:
        goal["goal_narrative"] = validated.narrative

    if key_results is not None:
        goal["metrics"] = key_results

    client = get_client()

    response = await client.post("/goal", json_data={"goals": [goal]})

    logger.info(
        "AUDIT: Objective created — name=%r, owner=%s, dates=%s to %s",
        validated.name,
        validated.owner,
        validated.start_date,
        validated.target_date,
    )

    return {"objective": response}
