"""Diff old vs new forge state to produce Event lists, with watch-mode filtering."""

from __future__ import annotations

import re
from datetime import datetime

from .models import MR, Event, EventType, Issue, Pipeline, Vulnerability

_MR_EVENT_TYPES = {
    EventType.MR_NEW_COMMENT,
    EventType.MR_APPROVED,
    EventType.MR_MERGED,
    EventType.MR_NEW_PUSH,
}

_PIPELINE_EVENT_TYPES = {
    EventType.PIPELINE_FAILED,
    EventType.PIPELINE_FIXED,
}

_VULN_EVENT_TYPES = {
    EventType.VULN_NEW,
    EventType.VULN_RESOLVED,
    EventType.REPORT_STALE_TOO_LONG,
}

_ISSUE_EVENT_TYPES = {
    EventType.ISSUE_OPENED,
    EventType.ISSUE_CLOSED,
    EventType.ISSUE_REOPENED,
    EventType.ISSUE_NEW_COMMENT,
    EventType.ISSUE_ASSIGNED,
}

_DEFAULT_BRANCHES = {"main", "master"}

_SEVERITY_ORDER = {"info": 0, "low": 1, "medium": 2, "high": 3, "critical": 4}


def _extract_vuln_severity(summary: str) -> str | None:
    """Extract severity from vuln event summary like "... (high)"."""
    m = re.search(r"\((\w+)\)\s*$", summary)
    return m.group(1).lower() if m else None


def _severity_meets_min(severity: str | None, min_severity: str) -> bool:
    """Return True if severity >= min_severity in the ordering."""
    if severity is None:
        return True  # can't determine → keep the event
    sev_rank = _SEVERITY_ORDER.get(severity, -1)
    min_rank = _SEVERITY_ORDER.get(min_severity, 0)
    return sev_rank >= min_rank


def _event(event_type: EventType, repo_name: str, summary: str) -> Event:
    return Event(id=None, type=event_type, repo_name=repo_name, summary=summary, detail="", created_at=datetime.now())


def _diff_single_mr(mr, prev, repo_name) -> list[Event]:
    """Compare one MR against its previous version and return any events."""
    events: list[Event] = []
    if mr.state == "merged" and prev.state != "merged":
        events.append(_event(EventType.MR_MERGED, repo_name,
                             f"MR !{mr.iid} '{mr.title}' merged"))
    if mr.user_notes_count > prev.user_notes_count:
        added = mr.user_notes_count - prev.user_notes_count
        events.append(_event(EventType.MR_NEW_COMMENT, repo_name,
                             f"MR !{mr.iid} '{mr.title}': {added} new comment(s)"))
    if mr.approved and not prev.approved:
        events.append(_event(EventType.MR_APPROVED, repo_name,
                             f"MR !{mr.iid} '{mr.title}' approved"))
    if mr.sha and prev.sha and mr.sha != prev.sha:
        events.append(_event(EventType.MR_NEW_PUSH, repo_name,
                             f"MR !{mr.iid} '{mr.title}': new push ({mr.sha[:8]})"))
    return events


def diff_mrs(old: list[MR], new: list[MR], repo_name: str) -> list[Event]:
    """Produce events by comparing old and new MR lists for a repo."""
    old_by_iid = {mr.iid: mr for mr in old}
    events: list[Event] = []
    for mr in new:
        prev = old_by_iid.get(mr.iid)
        if prev is not None:
            events.extend(_diff_single_mr(mr, prev, repo_name))
    return events


def diff_pipelines(old: list[Pipeline], new: list[Pipeline], repo_name: str) -> list[Event]:
    """Produce events by comparing old and new pipeline lists for a repo."""
    old_by_id = {p.id: p for p in old}
    events: list[Event] = []

    for pipeline in new:
        prev = old_by_id.get(pipeline.id)

        if prev is None:
            if pipeline.status == "failed":
                events.append(_event(EventType.PIPELINE_FAILED, repo_name,
                                     f"Pipeline #{pipeline.id} on '{pipeline.ref}' failed"))
            continue

        if pipeline.status == "failed" and prev.status != "failed":
            events.append(_event(EventType.PIPELINE_FAILED, repo_name,
                                 f"Pipeline #{pipeline.id} on '{pipeline.ref}' failed"))

        elif pipeline.status == "success" and prev.status == "failed":
            events.append(_event(EventType.PIPELINE_FIXED, repo_name,
                                 f"Pipeline #{pipeline.id} on '{pipeline.ref}' fixed"))

    return events


def diff_vulns(old: list[Vulnerability], new: list[Vulnerability], repo_name: str) -> list[Event]:
    """Produce events by comparing old and new vulnerability lists for a repo.

    Matches by ID first, then cancels out pairs where a vuln disappeared and
    reappeared under a new ID but the same name (common when pipelines re-scan).
    """
    old_ids = {v.id for v in old}
    new_ids = {v.id for v in new}
    new_by_id = {v.id: v for v in new}
    old_by_id = {v.id: v for v in old}

    appeared_ids = new_ids - old_ids
    disappeared_ids = old_ids - new_ids

    # Cancel out ID rotations: same vuln name disappeared and reappeared with a new ID
    appeared_names = {new_by_id[vid].name for vid in appeared_ids}
    disappeared_names = {old_by_id[vid].name for vid in disappeared_ids}
    rotated_names = appeared_names & disappeared_names

    events: list[Event] = []

    for vid in appeared_ids:
        v = new_by_id[vid]
        if v.name not in rotated_names:
            events.append(_event(EventType.VULN_NEW, repo_name,
                                 f"New vulnerability: {v.name} ({v.severity})"))

    for vid in disappeared_ids:
        v = old_by_id[vid]
        if v.name not in rotated_names:
            events.append(_event(EventType.VULN_RESOLVED, repo_name,
                                 f"Vulnerability resolved: {v.name} ({v.severity})"))

    return events


def _diff_single_issue(issue, prev, repo_name) -> list[Event]:
    """Compare one issue against its previous version and return any events."""
    events: list[Event] = []
    if issue.state == "closed" and prev.state != "closed":
        events.append(_event(EventType.ISSUE_CLOSED, repo_name,
                             f"Issue #{issue.iid} '{issue.title}' closed"))
    if issue.state == "opened" and prev.state == "closed":
        events.append(_event(EventType.ISSUE_REOPENED, repo_name,
                             f"Issue #{issue.iid} '{issue.title}' reopened"))
    if issue.user_notes_count > prev.user_notes_count:
        added = issue.user_notes_count - prev.user_notes_count
        events.append(_event(EventType.ISSUE_NEW_COMMENT, repo_name,
                             f"Issue #{issue.iid} '{issue.title}': {added} new comment(s)"))
    new_assignees = set(issue.assignees or []) - set(prev.assignees or [])
    if new_assignees:
        events.append(_event(EventType.ISSUE_ASSIGNED, repo_name,
                             f"Issue #{issue.iid} '{issue.title}': assigned to {', '.join(sorted(new_assignees))}"))
    return events


def diff_issues(old: list[Issue], new: list[Issue], repo_name: str) -> list[Event]:
    """Produce events by comparing old and new issue lists for a repo."""
    old_by_id = {i.id: i for i in old}
    events: list[Event] = []
    for issue in new:
        prev = old_by_id.get(issue.id)
        if prev is None:
            if old:
                events.append(_event(EventType.ISSUE_OPENED, repo_name,
                                     f"Issue #{issue.iid} '{issue.title}' opened"))
            continue
        events.extend(_diff_single_issue(issue, prev, repo_name))
    return events


def _extract_mr_iid(summary: str) -> int | None:
    """Extract MR iid from event summary like "MR !42 'title' ..."."""
    m = re.search(r"MR !(\d+)", summary)
    return int(m.group(1)) if m else None


def _extract_pipeline_ref(summary: str) -> str | None:
    """Extract pipeline ref from event summary like "Pipeline #123 on 'feat/x' ..."."""
    m = re.search(r"on '([^']+)'", summary)
    return m.group(1) if m else None


def _should_drop_mr(event: Event, mode: str, username: str, mr_by_iid: dict[int, MR]) -> bool:
    if mode == "none":
        return True
    if mode == "mine" and username:
        iid = _extract_mr_iid(event.summary)
        mr = mr_by_iid.get(iid) if iid is not None else None
        if mr and not mr.involves(username):
            return True
    return False


def _should_drop_pipeline(event: Event, mode: str, username: str, my_branches: set[str]) -> bool:
    if mode == "none":
        return True
    if mode == "mine" and username:
        ref = _extract_pipeline_ref(event.summary)
        if ref and ref not in my_branches:
            return True
    return False


def _extract_issue_iid(summary: str) -> int | None:
    """Extract issue iid from event summary like "Issue #42 'title' ..."."""
    m = re.search(r"Issue #(\d+)", summary)
    return int(m.group(1)) if m else None


def _should_drop_issue(event: Event, mode: str, username: str, issue_by_iid: dict[int, Issue]) -> bool:
    if mode == "none":
        return True
    if mode == "mine" and username:
        iid = _extract_issue_iid(event.summary)
        issue = issue_by_iid.get(iid) if iid is not None else None
        if issue and not issue.involves(username):
            return True
    return False


def _should_drop_vuln(event: Event, mode: str, min_severity: str) -> bool:
    if mode == "none":
        return True
    severity = _extract_vuln_severity(event.summary)
    return not _severity_meets_min(severity, min_severity)


def _build_my_branches(mrs, username):
    """Build the set of branches the user is involved with."""
    my_mrs = [m for m in mrs if m.involves(username)] if username else []
    branches = {m.source_branch for m in my_mrs}
    for m in my_mrs:
        branches.add(f"refs/merge-requests/{m.iid}/head")
    branches |= _DEFAULT_BRANCHES
    return branches


# Map event type categories to their handler key
_EVENT_CATEGORY = {}
for _et in _MR_EVENT_TYPES:
    _EVENT_CATEGORY[_et] = "mr"
for _et in _PIPELINE_EVENT_TYPES:
    _EVENT_CATEGORY[_et] = "pipeline"
for _et in _VULN_EVENT_TYPES:
    _EVENT_CATEGORY[_et] = "vuln"
for _et in _ISSUE_EVENT_TYPES:
    _EVENT_CATEGORY[_et] = "issue"


def _all_modes_pass(mrs_mode, pipelines_mode, vulns_mode, issues_mode, vulns_min_severity):
    return (mrs_mode == "all" and pipelines_mode == "all"
            and vulns_mode == "all" and issues_mode == "all"
            and vulns_min_severity == "info")


def filter_events(
    events: list[Event],
    mrs_mode: str,
    pipelines_mode: str,
    vulns_mode: str,
    username: str,
    mrs: list[MR],
    pipelines: list[Pipeline],  # pylint: disable=unused-argument
    issues_mode: str = "all",
    issues: list[Issue] | None = None,
    vulns_min_severity: str = "info",
) -> list[Event]:
    """Filter events based on watch mode config.

    Modes per category:
      "all"  - keep all events
      "mine" - keep only events related to the user
      "none" - drop all events of this category

    For vulns, an additional severity threshold is applied: only events
    whose severity >= vulns_min_severity are kept.

    AUTH_EXPIRED events always pass through.
    """
    if _all_modes_pass(mrs_mode, pipelines_mode, vulns_mode, issues_mode, vulns_min_severity):
        return events

    mr_by_iid = {mr.iid: mr for mr in mrs}
    my_branches = _build_my_branches(mrs, username)
    issue_by_iid = {i.iid: i for i in (issues or [])}

    ctx = {
        "mr": (mrs_mode, username, mr_by_iid),
        "pipeline": (pipelines_mode, username, my_branches),
        "vuln": (vulns_mode, vulns_min_severity),
        "issue": (issues_mode, username, issue_by_iid),
    }
    handlers = {
        "mr": _should_drop_mr,
        "pipeline": _should_drop_pipeline,
        "vuln": _should_drop_vuln,
        "issue": _should_drop_issue,
    }

    def should_drop(event: Event) -> bool:
        if event.type == EventType.AUTH_EXPIRED:
            return False
        category = _EVENT_CATEGORY.get(event.type)
        if category is None:
            return False
        return handlers[category](event, *ctx[category])

    return [e for e in events if not should_drop(e)]
