"""Universal doc header validation (Part 12.Q).

Manual docs should have a standardized header block:

    Status: active | archived | reference
    Owner: <team or person>
    Source-of-truth: CLI | schema | workflow | manual
    Last-reviewed: YYYY-MM-DD
    Superseded-by: <path or ADR>  # optional

This module validates that manual docs have these headers and that
the values are valid. Generated docs (CLI.md, CONFIG.md, WORKFLOWS.md)
and archived docs with superseded headers are exempt.
"""

from __future__ import annotations

import re
from pathlib import Path
from typing import TYPE_CHECKING

from .types import (
    DOC_HEADER_FIELDS,
    DOC_HEADER_OPTIONAL,
    DOC_VALID_SOURCES,
    DOC_VALID_STATUSES,
    SUPERSEDED_HEADER_PATTERNS,
    AuditFinding,
    FindingCategory,
    FindingSeverity,
)

if TYPE_CHECKING:
    pass

# Generated doc filenames that should NOT have manual headers
GENERATED_DOCS = frozenset(
    {
        "CLI.md",
        "CONFIG.md",
        "WORKFLOWS.md",
    }
)

# Directories containing generated or exempt docs
EXEMPT_DIRS = frozenset(
    {
        "docs/reference",  # Generated reference docs
        "docs/adr",  # ADRs have their own metadata format
    }
)

# Docs that are exempt from header requirements (README files, etc.)
EXEMPT_FILES = frozenset(
    {
        "README.md",
        "CHANGELOG.md",
    }
)

# Max lines to scan for header block
HEADER_SCAN_LINES = 30

# Valid header field names (case-insensitive matching)
VALID_HEADER_FIELD_NAMES = frozenset(
    {
        "status",
        "owner",
        "source-of-truth",
        "last-reviewed",
        "superseded-by",
        "priority",
        "depends-on",
        "can-parallel",
    }
)


def is_exempt_doc(file_path: Path, repo_root: Path) -> tuple[bool, str]:
    """Check if a doc is exempt from universal header requirements.

    Args:
        file_path: Path to the doc file
        repo_root: Repository root path

    Returns:
        Tuple of (is_exempt, reason)
    """
    rel_path = file_path.relative_to(repo_root)
    rel_str = str(rel_path)

    # Check if in exempt directory
    for exempt_dir in EXEMPT_DIRS:
        if rel_str.startswith(exempt_dir):
            return True, f"in exempt directory: {exempt_dir}"

    # Check if exempt file (README, CHANGELOG)
    if file_path.name in EXEMPT_FILES:
        return True, f"exempt file type: {file_path.name}"

    # Check if generated doc
    if file_path.name in GENERATED_DOCS:
        return True, f"generated doc: {file_path.name}"

    return False, ""


def has_superseded_header(content: str) -> bool:
    """Check if content has a superseded/archived header.

    Docs with superseded headers use a different format and are
    exempt from the universal header requirement.

    Args:
        content: File content (first N lines)

    Returns:
        True if superseded header pattern found
    """
    for line in content.split("\n")[:HEADER_SCAN_LINES]:
        for pattern in SUPERSEDED_HEADER_PATTERNS:
            if re.search(pattern, line):
                return True
    return False


def has_generated_banner(content: str) -> bool:
    """Check if content has a generated-doc banner.

    Generated docs have banners like:
    - "**Generated from:**"
    - "<!-- Generated by cihub docs generate -->"

    Args:
        content: File content (first N lines)

    Returns:
        True if generated banner found
    """
    generated_patterns = [
        r"\*\*Generated from:\*\*",
        r"<!--\s*Generated by",
        r"Auto-generated",
        r"DO NOT EDIT.*generated",
    ]
    for line in content.split("\n")[:HEADER_SCAN_LINES]:
        for pattern in generated_patterns:
            if re.search(pattern, line, re.IGNORECASE):
                return True
    return False


def parse_doc_header(content: str) -> dict[str, str]:
    """Parse header fields from doc content.

    Looks for a standardized header BLOCK at the top of the doc.
    The header block must:
    - Be at the beginning of the doc (after title/empty lines)
    - Contain consecutive header field lines
    - End when we hit content (---, ##, paragraph text)

    Only recognizes known header field names to avoid false positives
    from content like "**Status:** 80% complete" which is progress info,
    not doc metadata.

    Args:
        content: File content (first N lines)

    Returns:
        Dict mapping field names to values (only recognized header fields)
    """
    headers: dict[str, str] = {}
    lines = content.split("\n")[:HEADER_SCAN_LINES]

    # Patterns to match header fields
    header_patterns = [
        # **Status:** active
        r"\*\*([A-Za-z-]+):\*\*\s*(.+)",
        # Status: active
        r"^([A-Za-z-]+):\s*(.+)",
    ]

    # Patterns that indicate we've moved past the header block
    content_start_patterns = [
        r"^---",  # Horizontal rule
        r"^##",  # Subheading
        r"^>\s",  # Blockquote (these are often notes, not headers)
        r"^\|",  # Table
        r"^```",  # Code block
        r"^\d+\.",  # Numbered list
        r"^[-*]\s",  # Bullet list
    ]

    # Header block must be at the very top of the document.
    # Stop scanning on first non-header line (structural or plain text).

    for line in lines:
        stripped = line.strip()

        # Skip empty lines and title (# Heading)
        if not stripped or stripped.startswith("# "):
            continue

        # Check if we've hit structural content (table, code, list, etc.)
        for pattern in content_start_patterns:
            if re.match(pattern, stripped):
                # Hit a structural content marker - done scanning
                return headers

        # Try to parse as header field
        parsed_header = False
        for pattern in header_patterns:
            match = re.match(pattern, stripped)
            if match:
                field_name = match.group(1).strip()
                field_value = match.group(2).strip()
                # Only accept known header field names (case-insensitive)
                if field_name.lower() in VALID_HEADER_FIELD_NAMES:
                    # Header values shouldn't be super long (> 50 chars is likely content)
                    if len(field_value) <= 50:
                        headers[field_name] = field_value
                        parsed_header = True
                break

        if not parsed_header:
            # Line doesn't match header pattern - this is content
            # Headers must be at the very top; any non-header line ends scanning
            return headers

    return headers


def validate_header_value(field: str, value: str, file_path: str) -> list[AuditFinding]:
    """Validate a header field value.

    Args:
        field: Field name (case-insensitive comparison)
        value: Field value
        file_path: File path for error reporting

    Returns:
        List of findings for invalid values
    """
    findings: list[AuditFinding] = []
    field_lower = field.lower()

    if field_lower == "status":
        # Check if value starts with a valid status (case-insensitive)
        # This allows "ACTIVE - description" to match "active"
        value_lower = value.lower()
        is_valid = any(value_lower == s.lower() or value_lower.startswith(s.lower() + " ") for s in DOC_VALID_STATUSES)
        if not is_valid:
            findings.append(
                AuditFinding(
                    severity=FindingSeverity.WARNING,
                    category=FindingCategory.HEADER,
                    message=(f"Invalid Status value '{value}'. Valid: {', '.join(sorted(DOC_VALID_STATUSES))}"),
                    file=file_path,
                    code="CIHUB-HEADER-INVALID-STATUS",
                    suggestion=f"Use one of: {', '.join(sorted(DOC_VALID_STATUSES))}",
                )
            )

    elif field_lower == "source-of-truth":
        if value.lower() not in {s.lower() for s in DOC_VALID_SOURCES}:
            findings.append(
                AuditFinding(
                    severity=FindingSeverity.WARNING,
                    category=FindingCategory.HEADER,
                    message=(f"Invalid Source-of-truth value '{value}'. Valid: {', '.join(sorted(DOC_VALID_SOURCES))}"),
                    file=file_path,
                    code="CIHUB-HEADER-INVALID-SOURCE",
                    suggestion=f"Use one of: {', '.join(sorted(DOC_VALID_SOURCES))}",
                )
            )

    elif field_lower == "last-reviewed":
        # Validate date format YYYY-MM-DD
        if not re.match(r"^\d{4}-\d{2}-\d{2}$", value):
            findings.append(
                AuditFinding(
                    severity=FindingSeverity.WARNING,
                    category=FindingCategory.HEADER,
                    message=(f"Invalid Last-reviewed date format '{value}'. Expected: YYYY-MM-DD"),
                    file=file_path,
                    code="CIHUB-HEADER-INVALID-DATE",
                    suggestion="Use date format: YYYY-MM-DD (e.g., 2026-01-09)",
                )
            )

    return findings


def validate_doc_headers(repo_root: Path) -> list[AuditFinding]:
    """Validate universal headers on manual docs.

    Scans docs/ directory for manual docs and validates they have
    the required header fields with valid values.

    Args:
        repo_root: Repository root path

    Returns:
        List of audit findings
    """
    findings: list[AuditFinding] = []
    docs_dir = repo_root / "docs"

    if not docs_dir.exists():
        return findings

    # Scan all markdown files in docs/
    for md_file in docs_dir.rglob("*.md"):
        rel_path = str(md_file.relative_to(repo_root))

        # Check if exempt
        is_exempt, reason = is_exempt_doc(md_file, repo_root)
        if is_exempt:
            continue

        # Read content
        try:
            content = md_file.read_text(encoding="utf-8")
        except (OSError, UnicodeDecodeError):
            continue

        # Skip if has superseded header (uses different format)
        if has_superseded_header(content):
            continue

        # Skip if has generated banner
        if has_generated_banner(content):
            continue

        # Parse headers
        headers = parse_doc_header(content)

        # Flag docs with no header block (missing universal headers)
        if not headers:
            findings.append(
                AuditFinding(
                    severity=FindingSeverity.WARNING,
                    category=FindingCategory.HEADER,
                    message="Missing universal header block",
                    file=rel_path,
                    code="CIHUB-HEADER-MISSING-BLOCK",
                    suggestion=("Add header block at top: Status, Owner, Source-of-truth, Last-reviewed"),
                )
            )
            continue

        # Normalize header keys to lowercase for comparison
        headers_lower = {k.lower(): v for k, v in headers.items()}
        required_lower = {f.lower() for f in DOC_HEADER_FIELDS}

        # Check for missing required fields
        found_lower = set(headers_lower.keys())
        missing_lower = required_lower - found_lower
        # Convert back to display format
        missing_fields = {f for f in DOC_HEADER_FIELDS if f.lower() in missing_lower}

        # Flag incomplete headers (has some fields but not all required)
        if missing_fields:
            findings.append(
                AuditFinding(
                    severity=FindingSeverity.WARNING,
                    category=FindingCategory.HEADER,
                    message=(f"Incomplete header block - missing: {', '.join(sorted(missing_fields))}"),
                    file=rel_path,
                    code="CIHUB-HEADER-INCOMPLETE",
                    suggestion=("Complete header block with: Status, Owner, Source-of-truth, Last-reviewed"),
                )
            )

        # Validate existing field values
        required_lower = {f.lower() for f in DOC_HEADER_FIELDS}
        optional_lower = {f.lower() for f in DOC_HEADER_OPTIONAL}
        for field, value in headers.items():
            if field.lower() in required_lower or field.lower() in optional_lower:
                findings.extend(validate_header_value(field, value, rel_path))

    return findings


__all__ = [
    "GENERATED_DOCS",
    "EXEMPT_DIRS",
    "EXEMPT_FILES",
    "is_exempt_doc",
    "has_superseded_header",
    "has_generated_banner",
    "parse_doc_header",
    "validate_header_value",
    "validate_doc_headers",
]
