"""
Async analysis pipeline that fetches test results and components from the Maeris API,
extracts patterns from passing tests, and saves them to the feedback cache.
"""
import time
from collections import defaultdict

import structlog

from maeris_mcp.storage.feedback_cache import RepoFeedbackCache

logger = structlog.get_logger()

# Chunk size for batch API calls
_BATCH_CHUNK_SIZE = 50
# Max passed testcases to fetch components for
_MAX_COMPONENT_FETCH = 100
# Days after which a pattern's pass_count is halved during incremental merge
_DECAY_DAYS = 30


def _normalize_status(status: str) -> str:
    """Normalize backend test-result status values."""
    normalized = (status or "").strip().lower()
    if normalized in {"pass", "passed", "success"}:
        return "passed"
    if normalized in {"fail", "failed", "error"}:
        return "failed"
    return ""


async def analyze_test_results(
    client,
    cache: RepoFeedbackCache,
    incremental: bool = False,
    codemap_hash: str = "",
) -> None:
    """Fetch testcases, results, and components, extract patterns, and save to cache.

    Args:
        client: MaerisClient instance (already authenticated).
        cache: RepoFeedbackCache to update and save.
        incremental: If True, only fetch new results and merge with existing patterns.
        codemap_hash: Current codemap content hash for cache metadata.
    """
    entry = cache.load()

    # --- Step 1: Fetch all testcases ---
    testcases = await client.get_testcases()
    if not testcases:
        logger.info("feedback_analyzer.no_testcases")
        entry.last_analyzed = time.time()
        entry.testcase_count_at_analysis = 0
        entry.codemap_hash = codemap_hash
        cache.save()
        return

    tc_ids = []
    tc_name_map: dict[str, str] = {}
    for tc in testcases:
        tc_id = tc.get("_id") or tc.get("id") or tc.get("testcase_id") or ""
        if tc_id:
            tc_ids.append(tc_id)
            tc_name_map[tc_id] = tc.get("name") or tc.get("testcase_name") or tc.get("testcaseName") or ""

    if not tc_ids:
        logger.info("feedback_analyzer.no_tc_ids")
        entry.last_analyzed = time.time()
        entry.testcase_count_at_analysis = 0
        entry.codemap_hash = codemap_hash
        cache.save()
        return

    logger.info("feedback_analyzer.fetching_results", testcase_count=len(tc_ids))

    # --- Step 2: Fetch batch results (chunked) ---
    results_by_id: dict[str, list[dict]] = {}
    for i in range(0, len(tc_ids), _BATCH_CHUNK_SIZE):
        chunk = tc_ids[i : i + _BATCH_CHUNK_SIZE]
        try:
            chunk_results = await client.get_test_results_for_testcases(chunk)
            if isinstance(chunk_results, dict):
                results_by_id.update(chunk_results)
        except Exception as e:
            logger.warning("feedback_analyzer.batch_results_failed", error=str(e), chunk_start=i)

    # --- Step 3: Classify testcases ---
    passed_ids: list[str] = []
    failed_ids: list[str] = []
    flaky_ids: list[str] = []
    latest_result_ts = ""

    for tc_id in tc_ids:
        results = results_by_id.get(tc_id) or results_by_id.get(str(tc_id)) or []
        if not results:
            continue

        # Track newest result timestamp
        for r in results:
            ts = r.get("created_at") or ""
            if ts and ts > latest_result_ts:
                latest_result_ts = ts

        # Use latest result for classification
        latest_status = _normalize_status(results[0].get("status", ""))
        pass_count = sum(1 for r in results if _normalize_status(r.get("status", "")) == "passed")
        fail_count = sum(1 for r in results if _normalize_status(r.get("status", "")) == "failed")

        if latest_status == "passed" and fail_count == 0:
            passed_ids.append(tc_id)
        elif latest_status == "failed" and pass_count == 0:
            failed_ids.append(tc_id)
        elif pass_count > 0 and fail_count > 0:
            flaky_ids.append(tc_id)
        elif latest_status == "passed":
            passed_ids.append(tc_id)
        elif latest_status == "failed":
            failed_ids.append(tc_id)

    logger.info(
        "feedback_analyzer.classified",
        passed=len(passed_ids),
        failed=len(failed_ids),
        flaky=len(flaky_ids),
    )

    # --- Step 4: Fetch components + FLUs for passed tests (capped) ---
    fetch_ids = passed_ids[:_MAX_COMPONENT_FETCH]
    # Also fetch for flaky tests to identify flaky selectors
    flaky_fetch_ids = flaky_ids[:50]

    component_data_passed: dict = {}
    component_data_flaky: dict = {}
    flu_name_map: dict[str, str] = {}

    # Fetch FLUs for name matching
    try:
        flus = await client.get_flus()
        flu_name_map = {
            (f.get("flu_id") or f.get("id") or ""): (f.get("flu_name") or f.get("name") or "")
            for f in (flus or [])
            if isinstance(f, dict) and (f.get("flu_id") or f.get("id"))
        }
        logger.info("feedback_analyzer.flus_fetched", count=len(flu_name_map))
    except Exception as e:
        logger.warning("feedback_analyzer.flus_fetch_failed", error=str(e))

    if fetch_ids:
        try:
            component_data_passed = await client.get_components_for_testcases(fetch_ids)
        except Exception as e:
            logger.warning("feedback_analyzer.components_fetch_failed", error=str(e))

    if flaky_fetch_ids:
        try:
            component_data_flaky = await client.get_components_for_testcases(flaky_fetch_ids)
        except Exception as e:
            logger.warning("feedback_analyzer.flaky_components_fetch_failed", error=str(e))

    # --- Step 5: Extract patterns ---
    passed_steps = _extract_steps_by_testcase(component_data_passed, flu_name_map)
    flaky_steps = _extract_steps_by_testcase(component_data_flaky, flu_name_map)

    proven_selectors = _extract_proven_selectors(passed_steps, latest_result_ts)
    proven_nav_sequences = _extract_nav_sequences(passed_steps)
    page_pass_rates = _extract_page_pass_rates(passed_steps, flaky_steps, failed_ids, results_by_id)
    assertion_patterns = _extract_assertion_patterns(passed_steps)
    flaky_selectors = _extract_flaky_selectors(passed_steps, flaky_steps)

    # --- Step 6: For incremental, merge with existing ---
    if incremental:
        proven_selectors = _merge_selectors(entry.proven_selectors, proven_selectors)
        proven_nav_sequences = _merge_nav_sequences(entry.proven_nav_sequences, proven_nav_sequences)
        page_pass_rates = _merge_page_rates(entry.page_pass_rates, page_pass_rates)
        assertion_patterns = _merge_assertions(entry.assertion_patterns, assertion_patterns)
        flaky_selectors = _merge_flaky(entry.flaky_selectors, flaky_selectors)

    # --- Step 7: Update cache and save ---
    entry.last_analyzed = time.time()
    entry.last_result_created_at = latest_result_ts
    entry.testcase_count_at_analysis = len(tc_ids)
    entry.codemap_hash = codemap_hash
    entry.proven_selectors = proven_selectors
    entry.proven_nav_sequences = proven_nav_sequences
    entry.page_pass_rates = page_pass_rates
    entry.assertion_patterns = assertion_patterns
    entry.flaky_selectors = flaky_selectors
    cache.save()

    logger.info(
        "feedback_analyzer.complete",
        proven_selectors=len(proven_selectors),
        nav_sequences=len(proven_nav_sequences),
        flaky_selectors=len(flaky_selectors),
        pages_tracked=len(page_pass_rates),
    )


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------


def _extract_steps_by_testcase(
    component_data: dict,
    flu_name_map: dict[str, str] | None = None,
) -> dict[str, list[dict]]:
    """Parse the components-by-testcase-ids response into {tc_id: [step_dicts]}.

    Response shape:
      {
        "testcase_component_maps": [{testcase_id, component_ids: [...]}, ...],
        "unique_components": [{component_id/id, css_selector, action_taken, page_url, mock_input, ...}, ...]
      }
    """
    if not isinstance(component_data, dict):
        return {}

    flu_name_map = flu_name_map or {}
    tc_maps = component_data.get("testcase_component_maps") or []
    unique_comps = component_data.get("unique_components") or []

    comp_lookup: dict[str, dict] = {}
    for c in unique_comps:
        if not isinstance(c, dict):
            continue
        cid = c.get("component_id") or c.get("id") or ""
        if cid:
            comp_lookup[cid] = c

    result: dict[str, list[dict]] = {}
    for mapping in tc_maps:
        if not isinstance(mapping, dict):
            continue
        tc_id = mapping.get("testcase_id") or ""
        comp_ids = mapping.get("component_ids") or []
        if not tc_id:
            continue
        steps = []
        for cid in comp_ids:
            comp = comp_lookup.get(cid, {})
            if not comp:
                continue
            # Resolve FLU name from flu_id
            flu_id = comp.get("flu_id") or ""
            flu_name = flu_name_map.get(flu_id, "")
            # Component representation string (short description)
            comp_repr = (
                comp.get("component_representation_string")
                or comp.get("componentRepresentationString")
                or ""
            )
            # Use best available name: flu_name > comp name > comp_repr
            step_name = (
                flu_name
                or comp.get("name") or comp.get("flu_name")
                or comp_repr
                or ""
            )
            steps.append({
                "css_selector": comp.get("css_selector") or comp.get("cssSelector") or "",
                "action": comp.get("action_taken") or comp.get("action") or "",
                "page_url": comp.get("page_url") or comp.get("pageUrl") or "",
                "mock_input": comp.get("mock_input") or comp.get("mockInput") or "",
                "name": step_name,
                "component_repr": comp_repr,
                "assertion_type": comp.get("assertion_type") or "",
                "assertion_value": comp.get("assertion_value") or "",
                "assertion_nature": comp.get("assertion_nature") or "",
            })
        if steps:
            result[tc_id] = steps

    return result


def _extract_proven_selectors(
    passed_steps: dict[str, list[dict]],
    latest_ts: str,
) -> list[dict]:
    """Count (selector, action, page_url) tuples across passed tests."""
    # Track counts and extra context for each unique (selector, action, page_url)
    counter: dict[tuple[str, str, str], int] = defaultdict(int)
    context: dict[tuple[str, str, str], dict] = {}

    for steps in passed_steps.values():
        for step in steps:
            selector = step.get("css_selector", "").strip()
            action = step.get("action", "").strip()
            page_url = step.get("page_url", "").strip()
            if selector and action:
                key = (selector, action, page_url)
                counter[key] += 1
                # Store first seen mock_input, name, and component_repr for matching context
                if key not in context:
                    context[key] = {
                        "mock_input": step.get("mock_input", ""),
                        "name": step.get("name", ""),
                        "component_repr": step.get("component_repr", ""),
                    }

    return [
        {
            "selector": sel,
            "action": act,
            "page_url": page,
            "pass_count": count,
            "last_seen": latest_ts[:10] if latest_ts else "",
            "mock_input": context.get((sel, act, page), {}).get("mock_input", ""),
            "name": context.get((sel, act, page), {}).get("name", ""),
            "component_repr": context.get((sel, act, page), {}).get("component_repr", ""),
        }
        for (sel, act, page), count in sorted(counter.items(), key=lambda x: x[1], reverse=True)
    ]


def _extract_nav_sequences(passed_steps: dict[str, list[dict]]) -> list[dict]:
    """Extract page_url transitions (edges) across passed tests."""
    edge_counter: dict[tuple[str, str], dict] = {}

    for steps in passed_steps.values():
        pages_seen: list[str] = []
        transition_steps: list[list[dict]] = []

        current_page = ""
        current_transition: list[dict] = []

        for step in steps:
            page = step.get("page_url", "").strip()
            if page and page != current_page:
                if current_page:
                    pages_seen.append(current_page)
                    transition_steps.append(current_transition)
                current_page = page
                current_transition = []
            if step.get("css_selector") and step.get("action"):
                current_transition.append({
                    "action": step["action"],
                    "selector": step["css_selector"],
                })

        # Record edges
        for i, from_page in enumerate(pages_seen):
            to_page = pages_seen[i + 1] if i + 1 < len(pages_seen) else current_page
            if not to_page or from_page == to_page:
                continue
            key = (from_page, to_page)
            if key not in edge_counter:
                edge_counter[key] = {
                    "from_page": from_page,
                    "to_page": to_page,
                    "steps": transition_steps[i][:5] if i < len(transition_steps) else [],
                    "pass_count": 0,
                }
            edge_counter[key]["pass_count"] += 1

    return sorted(edge_counter.values(), key=lambda x: x.get("pass_count", 0), reverse=True)


def _extract_page_pass_rates(
    passed_steps: dict[str, list[dict]],
    flaky_steps: dict[str, list[dict]],
    failed_ids: list[str],
    results_by_id: dict[str, list[dict]],
) -> dict[str, dict]:
    """Calculate pass rate per page_url."""
    page_stats: dict[str, dict[str, int]] = defaultdict(lambda: {"total": 0, "passed": 0})

    # Pages from passed tests
    for steps in passed_steps.values():
        pages_in_test = {s.get("page_url", "").strip() for s in steps if s.get("page_url", "").strip()}
        for page in pages_in_test:
            page_stats[page]["total"] += 1
            page_stats[page]["passed"] += 1

    # Pages from flaky tests
    for steps in flaky_steps.values():
        pages_in_test = {s.get("page_url", "").strip() for s in steps if s.get("page_url", "").strip()}
        for page in pages_in_test:
            page_stats[page]["total"] += 1
            # Count as partial pass (0.5) — we don't know which runs passed on which page

    # Failed tests contribute to total but not passed (no component data, so use pages from other tests)

    return {
        page: {
            "total": stats["total"],
            "passed": stats["passed"],
            "rate": round(stats["passed"] / stats["total"] * 100, 1) if stats["total"] > 0 else 0,
        }
        for page, stats in page_stats.items()
        if page
    }


def _extract_assertion_patterns(passed_steps: dict[str, list[dict]]) -> list[dict]:
    """Find assertion type/value combos that consistently pass."""
    counter: dict[tuple[str, str, str], int] = defaultdict(int)

    for steps in passed_steps.values():
        for step in steps:
            a_type = step.get("assertion_type", "").strip()
            a_value = step.get("assertion_value", "").strip()
            page_url = step.get("page_url", "").strip()
            if a_type:
                counter[(page_url, a_type, a_value)] += 1

    return [
        {
            "page_url": page,
            "assertion_type": a_type,
            "assertion_value": a_value,
            "pass_count": count,
        }
        for (page, a_type, a_value), count in sorted(counter.items(), key=lambda x: x[1], reverse=True)
    ]


def _extract_flaky_selectors(
    passed_steps: dict[str, list[dict]],
    flaky_steps: dict[str, list[dict]],
) -> list[dict]:
    """Identify selectors that appear in both passed and flaky tests."""
    # Count selector appearances in passed tests
    passed_counter: dict[tuple[str, str], int] = defaultdict(int)
    for steps in passed_steps.values():
        for step in steps:
            sel = step.get("css_selector", "").strip()
            page = step.get("page_url", "").strip()
            if sel:
                passed_counter[(sel, page)] += 1

    # Count selector appearances in flaky tests
    flaky_counter: dict[tuple[str, str], int] = defaultdict(int)
    for steps in flaky_steps.values():
        for step in steps:
            sel = step.get("css_selector", "").strip()
            page = step.get("page_url", "").strip()
            if sel:
                flaky_counter[(sel, page)] += 1

    # Flaky = appears in flaky tests
    result = []
    for (sel, page), flaky_count in flaky_counter.items():
        pass_count = passed_counter.get((sel, page), 0)
        total = pass_count + flaky_count
        if total > 0:
            flakiness = round(flaky_count / total * 100, 1)
            result.append({
                "selector": sel,
                "page_url": page,
                "pass_count": pass_count,
                "fail_count": flaky_count,
                "flakiness_score": flakiness,
            })

    return sorted(result, key=lambda x: x.get("flakiness_score", 0), reverse=True)


# ---------------------------------------------------------------------------
# Incremental merge helpers
# ---------------------------------------------------------------------------


def _merge_selectors(existing: list[dict], new: list[dict]) -> list[dict]:
    """Merge proven selectors, incrementing counts for known entries."""
    lookup: dict[tuple[str, str, str], dict] = {}
    for s in existing:
        key = (s.get("selector", ""), s.get("action", ""), s.get("page_url", ""))
        # Decay old entries
        age_days = (time.time() - _parse_date_to_epoch(s.get("last_seen", ""))) / 86400
        if age_days > _DECAY_DAYS:
            s["pass_count"] = max(1, s.get("pass_count", 1) // 2)
        lookup[key] = s

    for s in new:
        key = (s.get("selector", ""), s.get("action", ""), s.get("page_url", ""))
        if key in lookup:
            lookup[key]["pass_count"] = lookup[key].get("pass_count", 0) + s.get("pass_count", 0)
            lookup[key]["last_seen"] = s.get("last_seen", lookup[key].get("last_seen", ""))
        else:
            lookup[key] = s

    return sorted(lookup.values(), key=lambda x: x.get("pass_count", 0), reverse=True)


def _merge_nav_sequences(existing: list[dict], new: list[dict]) -> list[dict]:
    """Merge navigation sequences."""
    lookup: dict[tuple[str, str], dict] = {}
    for n in existing:
        key = (n.get("from_page", ""), n.get("to_page", ""))
        lookup[key] = n
    for n in new:
        key = (n.get("from_page", ""), n.get("to_page", ""))
        if key in lookup:
            lookup[key]["pass_count"] = lookup[key].get("pass_count", 0) + n.get("pass_count", 0)
        else:
            lookup[key] = n
    return sorted(lookup.values(), key=lambda x: x.get("pass_count", 0), reverse=True)


def _merge_page_rates(existing: dict, new: dict) -> dict:
    """Merge page pass rates — new data replaces old since it's a fresh count."""
    merged = dict(existing)
    merged.update(new)
    return merged


def _merge_assertions(existing: list[dict], new: list[dict]) -> list[dict]:
    """Merge assertion patterns."""
    lookup: dict[tuple[str, str, str], dict] = {}
    for a in existing:
        key = (a.get("page_url", ""), a.get("assertion_type", ""), a.get("assertion_value", ""))
        lookup[key] = a
    for a in new:
        key = (a.get("page_url", ""), a.get("assertion_type", ""), a.get("assertion_value", ""))
        if key in lookup:
            lookup[key]["pass_count"] = lookup[key].get("pass_count", 0) + a.get("pass_count", 0)
        else:
            lookup[key] = a
    return sorted(lookup.values(), key=lambda x: x.get("pass_count", 0), reverse=True)


def _merge_flaky(existing: list[dict], new: list[dict]) -> list[dict]:
    """Merge flaky selectors."""
    lookup: dict[tuple[str, str], dict] = {}
    for f in existing:
        key = (f.get("selector", ""), f.get("page_url", ""))
        lookup[key] = f
    for f in new:
        key = (f.get("selector", ""), f.get("page_url", ""))
        if key in lookup:
            lookup[key]["pass_count"] = lookup[key].get("pass_count", 0) + f.get("pass_count", 0)
            lookup[key]["fail_count"] = lookup[key].get("fail_count", 0) + f.get("fail_count", 0)
            total = lookup[key]["pass_count"] + lookup[key]["fail_count"]
            lookup[key]["flakiness_score"] = round(lookup[key]["fail_count"] / total * 100, 1) if total else 0
        else:
            lookup[key] = f
    return sorted(lookup.values(), key=lambda x: x.get("flakiness_score", 0), reverse=True)


def _parse_date_to_epoch(date_str: str) -> float:
    """Parse YYYY-MM-DD or ISO timestamp to epoch. Returns 0 on failure."""
    if not date_str:
        return 0.0
    try:
        import datetime
        if "T" in date_str:
            dt = datetime.datetime.fromisoformat(date_str.replace("Z", "+00:00"))
        else:
            dt = datetime.datetime.strptime(date_str[:10], "%Y-%m-%d")
        return dt.timestamp()
    except Exception:
        return 0.0
