"""Run the repeatable static gates for one completed CM108G unit."""
from __future__ import annotations
from html import unescape
from html.parser import HTMLParser
from pathlib import Path
from xml.etree import ElementTree
import json
import re
import sys

ROOT = Path(__file__).resolve().parents[1]
SVG_BY_WEEK = {1:"evidence-inference-decision.svg",2:"managerial-role-map.svg",3:"decision-memo-flow.svg",4:"week-04-concept-map.svg",5:"goals-planning-architecture.svg",6:"strategy-diversification-architecture.svg"}

class P(HTMLParser):
    def __init__(self):
        super().__init__()
        self.ids = {}
        self.frags = []
        self.headings = []
        self.heading_texts = []
        self.fonts = []
        self.actions = []
        self.main = False
        self.tools = False
        self.skip_link = False
        self.concept_scroll_region = False
        self.notes_modal = False
        self.table_wrap_count = 0
        self.accessible_table_wrap_count = 0
        self.external_links = []
        self.anchor_depth = 0
        self.nested_anchor_count = 0
        self._anchor_href = None
        self._anchor_parts = []
        self._heading_level = None
        self._heading_parts = []

    def handle_starttag(self, tag, attrs):
        a = {k.lower(): v or "" for k, v in attrs}
        classes = set(a.get("class", "").split())
        if tag == "a":
            if self.anchor_depth:
                self.nested_anchor_count += 1
            self.anchor_depth += 1
        if "id" in a:
            self.ids[a["id"]] = self.ids.get(a["id"], 0) + 1
        if tag == "a" and a.get("href", "").startswith("#"):
            self.frags.append(a["href"][1:])
        if tag == "a" and a.get("href", "") and not a.get("href", "").startswith("#"):
            self._anchor_href = a["href"]
            self._anchor_parts = []
        if tag == "a" and "skip-link" in classes and a.get("href") == "#reading":
            self.skip_link = True
        if tag in {"h1", "h2", "h3", "h4", "h5", "h6"}:
            self._heading_level = int(tag[1])
            self._heading_parts = []
            self.headings.append(self._heading_level)
        if tag == "button" and a.get("data-font-size"):
            self.fonts.append(a["data-font-size"])
        if a.get("data-study-action"):
            self.actions.append(a["data-study-action"])
        if "concept-figure__graphic" in classes:
            self.concept_scroll_region = (
                a.get("tabindex") == "0" and a.get("role") == "region" and bool(a.get("aria-label"))
            )
        if "table-wrap" in classes:
            self.table_wrap_count += 1
            if a.get("tabindex") == "0" and a.get("role") == "region" and bool(a.get("aria-label")):
                self.accessible_table_wrap_count += 1
        if a.get("id") == "notes-panel":
            self.notes_modal = a.get("role") == "dialog" and a.get("aria-modal") == "true"
        self.main |= a.get("data-count") == "main-body"
        self.tools |= "data-study-tools" in a

    def handle_data(self, data):
        if self._heading_level is not None:
            self._heading_parts.append(data)
        if self._anchor_href is not None:
            self._anchor_parts.append(data)

    def handle_endtag(self, tag):
        if tag == "a":
            self.anchor_depth = max(0, self.anchor_depth - 1)
        if tag == "a" and self._anchor_href is not None:
            self.external_links.append((self._anchor_href, " ".join("".join(self._anchor_parts).split())))
            self._anchor_href = None
            self._anchor_parts = []
        if self._heading_level is not None and tag == f"h{self._heading_level}":
            self.heading_texts.append((self._heading_level, " ".join("".join(self._heading_parts).split())))
            self._heading_level = None
            self._heading_parts = []

def raw_markdown_words(text):
    """Legacy audit count; Markdown destinations may inflate this number."""
    return len(re.findall(r"\b[A-Za-z]+(?:['’][A-Za-z]+)?\b", text))


def visible_markdown(text):
    """Return learner-visible Markdown text without URLs, tags, or comments."""
    text = re.sub(r"<!--.*?-->", " ", text, flags=re.S)
    text = re.sub(r"!?\[([^\]]*)\]\((?:[^()]|\([^()]*\))*\)", r"\1", text)
    text = re.sub(r"<https?://[^>]+>", " ", text)
    text = re.sub(r"https?://[^\s)\]>]+", " ", text)
    text = re.sub(r"<[^>]+>", " ", text)
    return text


def visible_words(text):
    """Conservative English count: a hyphenated compound is one visible word."""
    return len(re.findall(r"\b[A-Za-z]+(?:[-'’][A-Za-z]+)*\b", visible_markdown(text)))


def normalized(text):
    return " ".join(unescape(re.sub(r"<[^>]+>", " ", text)).split()).casefold()

def run(week):
    unit=f"week-{week:02d}"; module=ROOT/"04_modules"/unit; page=ROOT/"06_web/pages"/f"{unit}.html"; svg_path=ROOT/"06_web/assets/svg"/SVG_BY_WEEK.get(week,f"{unit}-concept-map.svg")
    manuscript=(module/"manuscript.md").read_text(encoding="utf-8"); resources=(module/"resources.md").read_text(encoding="utf-8"); glossary=(module/"glossary.md").read_text(encoding="utf-8"); assessment=(module/"assessment.md").read_text(encoding="utf-8"); m=re.search(r"<!-- main-body:start -->(.*?)<!-- main-body:end -->",manuscript,re.S)
    if not m: raise ValueError(f"{unit}: main-body markers missing")
    body=m.group(1); ptext=page.read_text(encoding="utf-8"); p=P(); p.feed(ptext); css=(ROOT/"06_web/css/textbook.css").read_text(encoding="utf-8"); reader_js=(ROOT/"06_web/js/textbook-reader-v2.js").read_text(encoding="utf-8"); svg=svg_path.read_text(encoding="utf-8"); root=ElementTree.fromstring(svg); a={k.rsplit("}",1)[-1].lower():v for k,v in root.attrib.items()}
    forbidden = ("Chief PM", "Research Agent", "Writing Agent", "Reviewer Agent", "Context Packet", "Claim Ledger", "C:\\Users\\", "pm_state.json")
    expected = {"toggle-notes", "print", "open-export", "highlight", "underline", "show-note", "download-notes", "download-reading", "clear-record"}
    heading_values = [value.casefold() for _, value in p.heading_texts]
    svg_labels = []
    for node in root.iter():
        if node.tag.rsplit("}", 1)[-1] == "text":
            value = " ".join("".join(node.itertext()).split())
            if value and value not in svg_labels:
                svg_labels.append(value)
    alternative_match = re.search(r'<div class="concept-figure__alternative"[^>]*>(.*?)</div>', ptext, re.S)
    alternative_text = normalized(alternative_match.group(1)) if alternative_match else ""
    svg_labels_gate = bool(svg_labels) and all(normalized(label) in alternative_text for label in svg_labels)
    if week <= 5:
        wrapper = (ROOT / "scripts" / f"build_week{week}_html.py").read_text(encoding="utf-8")
        canonical_builder_gate = "def build_legacy" in wrapper and f"return build_unit({week})" in wrapper
    else:
        canonical_builder_gate = True
    controlled_reference_links = (
        ("https://d21.co.jp/book/detail/978-4-7993-2555-1", "Discover 21 publisher record"),
        ("https://www.waseda.jp/fcom/wbs/faculty-en/15632", "Waseda Business School author profile"),
    )
    reference_book_descriptive_link_gate = all(
        f"[{label}]({href})" in resources
        and any(link_href == href and link_text == label and link_text != link_href for link_href, link_text in p.external_links)
        for href, label in controlled_reference_links
    )
    result = {
        "unit": unit,
        "word_count_method": "learner-visible English; link labels retained; URL destinations/tags/comments excluded; hyphenated compounds count as one",
        "main_body_word_count": visible_words(body),
        "raw_markdown_main_body_word_count": raw_markdown_words(body),
        "supporting_content_word_count": sum(visible_words(text) for text in (glossary, assessment, resources)),
        "raw_markdown_supporting_content_word_count": sum(raw_markdown_words(text) for text in (glossary, assessment, resources)),
        "missing_fragments": sorted(set(p.frags) - set(p.ids)),
        "duplicate_ids": sorted(key for key, value in p.ids.items() if value > 1),
        "word_gate": visible_words(body) >= 5000,
        "authoring_buffer_gate": visible_words(body) >= 5100,
        "heading_gate": p.headings.count(1) == 1,
        "heading_hierarchy_gate": all((later - earlier) <= 1 for earlier, later in zip(p.headings, p.headings[1:])),
        "one_learning_map_heading_gate": sum("learning map" in value for value in heading_values) == 1,
        "one_no_ai_heading_gate": sum("no ai task" in value for value in heading_values) == 1,
        "one_glossary_heading_gate": sum("glossary" in value for value in heading_values) == 1,
        "glossary_term_count": len(re.findall(r"(?m)^(?:\d+\.\s+\*\*|##\s+\d+\.)", glossary)),
        "glossary_count_gate": 1 <= len(re.findall(r"(?m)^(?:\d+\.\s+\*\*|##\s+\d+\.)", glossary)) <= 15,
        "assessment_question_count": len(re.findall(r"(?m)^##\s+(?:Question\s+)?\d+(?=\b|[.:|])", assessment)),
        "assessment_count_gate": len(re.findall(r"(?m)^##\s+(?:Question\s+)?\d+(?=\b|[.:|])", assessment)) == 3,
        "font_gate": set(p.fonts) >= {"xs", "s", "m", "l", "xl"},
        "study_tools": p.tools,
        "study_action_gate": expected <= set(p.actions),
        "main_body_marker_gate": p.main,
        "single_column_css_gate": "column-count: 2" not in css and "column-count: 1" in css,
        "reader_v2_gate": "../js/textbook-reader-v2.js" in ptext,
        "stable_annotation_gate": all(token in reader_js for token in ("stableHash", "reanchorSegment", "blockId", "start", "end", "quote", "itemMatchesPage")),
        "keyboard_selection_gate": all(token in reader_js for token in ("keyboardSelectionPending", "selectionNavigationKeys", "focusToolbar", "selectionTools.contains(document.activeElement)")),
        "focus_management_gate": all(token in reader_js for token in ("focusableElements", "lastNotesFocus", "lastDialogFocus", 'setAttribute("aria-modal", "true")')),
        "local_date_export_gate": "localDateStamp(new Date())" in reader_js and "toISOString().slice(0, 10)" not in reader_js,
        "responsive_contents_gate": "Show page contents" in reader_js and "@media (max-width: 900px)" in css,
        "mobile_concept_scroll_gate": ".concept-figure img, .concept-figure svg { width: 760px" in css,
        "svg_font_control_gate": "--diagram-text-size" in css and ".concept-figure__graphic svg text" in css,
        "skip_link_gate": p.skip_link and ".skip-link:focus" in css,
        "concept_scroll_region_gate": p.concept_scroll_region,
        "table_wrap_count": p.table_wrap_count,
        "accessible_table_wrap_count": p.accessible_table_wrap_count,
        "table_accessibility_gate": p.table_wrap_count > 0 and p.accessible_table_wrap_count == p.table_wrap_count,
        "notes_modal_gate": p.notes_modal,
        "print_notes_gate": "print-notes" in reader_js and ".print-notes { display: block" in css and "break-after: avoid-page" in css,
        "privacy_gate": not any(value.casefold() in ptext.casefold() for value in forbidden),
        "learner_internal_status_gate": not any(value in ptext.casefold() for value in (
            "course edition under review", "this unit is an english course edition under review",
            "reviewer-cleared", "instructor-approved", "review candidate",
            "pending instructor approval", "approved public-source boundary",
        )),
        "answer_key_leakage_gate": not any(value in ptext.casefold() for value in (
            "answer key", "correct choices", "correct answers", "instructor answer", "scoring guide",
        )),
        "instructor_only_leakage_gate": not any(value in ptext.casefold() for value in (
            "instructor-only:start", "instructor-only:end",
            "instructor rationale and scoring guide",
            "exclude the following instructor-only block",
        )),
        "book_url_not_exposed_gate": "docs.google.com/document" not in ptext and "docs.google.com/document" not in resources,
        "reference_book_title_gate": "the new business of management" not in (resources + "\n" + ptext).casefold(),
        "reference_book_attribution_gate": all(value in resources.casefold() for value in (
            "koji mitani", "すべての働く人のための新しい経営学", "the instructor",
            "target", "value", "capability", "revenue model", "teacher-provided",
            "management theory", "research evidence", "application", "practice",
        )) and "inspir" in resources.casefold() and "not a translation" in resources.casefold()
        and "official english edition" in resources.casefold() and "substitute" in resources.casefold(),
        "reference_book_descriptive_link_gate": reference_book_descriptive_link_gate,
        "valid_anchor_nesting_gate": p.nested_anchor_count == 0 and p.anchor_depth == 0,
        "inline_citation_gate": len(re.findall(r"https://doi\.org/", body)) >= 3,
        "anti_ai_punctuation_gate": "—" not in body and "–" not in body,
        "svg_gate": root.tag.endswith("svg") and bool(a.get("viewbox")) and a.get("role") == "img" and bool(a.get("aria-label")),
        "svg_text_alternative_gate": bool(alternative_match) and "how the elements connect:" in alternative_text and svg_labels_gate,
        "canonical_builder_gate": canonical_builder_gate,
    }
    required = [
        "word_gate", "heading_gate", "heading_hierarchy_gate", "one_learning_map_heading_gate",
        "one_no_ai_heading_gate", "one_glossary_heading_gate", "glossary_count_gate", "assessment_count_gate", "font_gate", "study_tools",
        "study_action_gate", "main_body_marker_gate", "single_column_css_gate", "reader_v2_gate",
        "stable_annotation_gate", "keyboard_selection_gate", "focus_management_gate", "local_date_export_gate",
        "responsive_contents_gate", "mobile_concept_scroll_gate", "svg_font_control_gate", "skip_link_gate",
        "concept_scroll_region_gate", "table_accessibility_gate", "notes_modal_gate", "print_notes_gate", "privacy_gate", "learner_internal_status_gate", "answer_key_leakage_gate",
        "instructor_only_leakage_gate",
        "book_url_not_exposed_gate", "reference_book_title_gate", "reference_book_attribution_gate", "reference_book_descriptive_link_gate", "valid_anchor_nesting_gate", "inline_citation_gate", "anti_ai_punctuation_gate", "svg_gate",
        "svg_text_alternative_gate", "canonical_builder_gate",
    ]
    result["total_counted_word_count"]=result["main_body_word_count"]+result["supporting_content_word_count"]; result["recommendation"]="pass-static-gates" if all(result[k] for k in required) and not result["missing_fragments"] and not result["duplicate_ids"] else "blocked"
    out=ROOT/"07_tests"; out.mkdir(exist_ok=True); (out/f"{unit}-static-qa.json").write_text(json.dumps(result,indent=2),encoding="utf-8",newline="\n"); (out/f"{unit}-static-qa.md").write_text(f"# {unit} static QA\n\n"+"\n".join(f"- **{k}:** {v}" for k,v in result.items())+"\n\nStatic QA does not replace independent Strict Review, browser interaction review, print review, or instructor approval.\n",encoding="utf-8",newline="\n"); print(json.dumps(result,indent=2))

if __name__=="__main__":
    if len(sys.argv)!=2 or not sys.argv[1].isdigit(): raise SystemExit("Usage: python scripts/qa_unit.py WEEK")
    run(int(sys.argv[1]))
