"""Code to handle the output of PEP 0."""

from __future__ import annotations

from typing import TYPE_CHECKING
import unicodedata

from pep_sphinx_extensions.pep_processor.transforms.pep_headers import ABBREVIATED_STATUSES
from pep_sphinx_extensions.pep_processor.transforms.pep_headers import ABBREVIATED_TYPES
from pep_sphinx_extensions.pep_zero_generator.constants import DEAD_STATUSES
from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_ACCEPTED
from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_ACTIVE
from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_DEFERRED
from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_DRAFT
from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_FINAL
from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_PROVISIONAL
from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_REJECTED
from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_VALUES
from pep_sphinx_extensions.pep_zero_generator.constants import STATUS_WITHDRAWN
from pep_sphinx_extensions.pep_zero_generator.constants import SUBINDICES_BY_TOPIC
from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_INFO
from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_PROCESS
from pep_sphinx_extensions.pep_zero_generator.constants import TYPE_VALUES
from pep_sphinx_extensions.pep_zero_generator.errors import PEPError

if TYPE_CHECKING:
    from pep_sphinx_extensions.pep_zero_generator.parser import PEP

HEADER = """\
PEP: 0
Title: Index of Python Enhancement Proposals (PEPs)
Author: The PEP Editors
Status: Active
Type: Informational
Content-Type: text/x-rst
Created: 13-Jul-2000
"""

INTRO = """\
This PEP contains the index of all Python Enhancement Proposals,
known as PEPs.  PEP numbers are :pep:`assigned <1#pep-editors>`
by the PEP editors, and once assigned are never changed.  The
`version control history <https://github.com/python/peps>`_ of
the PEP texts represent their historical record.
"""


class PEPZeroWriter:
    # This is a list of reserved PEP numbers.  Reservations are not to be used for
    # the normal PEP number allocation process - just give out the next available
    # PEP number.  These are for "special" numbers that may be used for semantic,
    # humorous, or other such reasons, e.g. 401, 666, 754.
    #
    # PEP numbers may only be reserved with the approval of a PEP editor.  Fields
    # here are the PEP number being reserved and the claimants for the PEP.
    # Although the output is sorted when PEP 0 is generated, please keep this list
    # sorted as well.
    RESERVED = {
        801: "Warsaw",
    }

    def __init__(self, release_peps: dict[str, int] | None = None):
        self.output: list[str] = []
        self.release_peps = release_peps or {}

    def emit_text(self, content: str) -> None:
        # Appends content argument to the output list
        self.output.append(content)

    def emit_newline(self) -> None:
        self.output.append("")

    def emit_author_table_separator(self, max_name_len: int) -> None:
        author_table_separator = "=" * max_name_len + "  " + "=" * len("email address")
        self.output.append(author_table_separator)

    def emit_pep_row(
        self,
        *,
        shorthand: str,
        number: int,
        title: str,
        authors: str,
        python_version: str | None = None,
    ) -> None:
        self.emit_text(f"   * - {shorthand}")
        self.emit_text(f"     - :pep:`{number} <{number}>`")
        self.emit_text(f"     - :pep:`{title.replace('`', '')} <{number}>`")
        self.emit_text(f"     - {authors}")
        if python_version is not None:
            linked_versions = []

            for version in map(str.strip, python_version.split(",")):
                release_pep = self.release_peps.get(version)

                if release_pep is not None:
                    linked_versions.append(f":pep:`{version} <{release_pep}>`")
                else:
                    linked_versions.append(version)

            self.emit_text(f"     - {', '.join(linked_versions)}")

    def emit_column_headers(self, *, include_version=True) -> None:
        """Output the column headers for the PEP indices."""
        self.emit_text(".. list-table::")
        self.emit_text("   :header-rows: 1")
        self.emit_text("   :widths: auto")
        self.emit_text("   :class: pep-zero-table")
        self.emit_newline()
        self.emit_text("   * - ")
        self.emit_text("     - PEP")
        self.emit_text("     - Title")
        self.emit_text("     - Authors")
        if include_version:
            self.emit_text("     - ")  # for Python-Version

    def emit_title(self, text: str, *, symbol: str = "=") -> None:
        self.output.append(text)
        self.output.append(symbol * len(text))
        self.emit_newline()

    def emit_subtitle(self, text: str) -> None:
        self.emit_title(text, symbol="-")

    def emit_table(self, peps: list[PEP]) -> None:
        include_version = any(pep.details["python_version"] for pep in peps)
        self.emit_column_headers(include_version=include_version)
        for pep in peps:
            details = pep.details
            if not include_version:
                details.pop("python_version")
            self.emit_pep_row(**details)

    def emit_pep_category(self, category: str, peps: list[PEP]) -> None:
        self.emit_subtitle(category)
        self.emit_table(peps)
        # list-table must have at least one body row
        if len(peps) == 0:
            self.emit_text("   * -")
            self.emit_text("     -")
            self.emit_text("     -")
            self.emit_text("     -")
            self.emit_text("     -")
        self.emit_newline()

    def write_numerical_index(self, peps: list[PEP]) -> str:
        """Write PEPs by number."""
        self.emit_text(".. _numerical-index:")
        self.emit_newline()

        self.emit_title("Numerical Index")
        self.emit_table(peps)
        self.emit_newline()

        numerical_index_string = "\n".join(self.output)
        return numerical_index_string

    def write_pep0(
        self,
        peps: list[PEP],
        header: str = HEADER,
        intro: str = INTRO,
        is_pep0: bool = True,
        builder: str = None,
    ) -> str:
        if len(peps) == 0:
            return ""

        # PEP metadata
        self.emit_text(header)
        self.emit_newline()

        # Introduction
        self.emit_title("Introduction")
        self.emit_text(intro)
        self.emit_newline()

        # PEPs by topic
        if is_pep0:
            self.emit_title("Topics")
            self.emit_text(
                "PEPs for specialist subjects are :doc:`indexed by topic <topic/index>`."
            )
            self.emit_newline()
            for subindex in SUBINDICES_BY_TOPIC:
                target = (
                    f"topic/{subindex}.html"
                    if builder == "html"
                    else f"../topic/{subindex}/"
                )
                self.emit_text(f"* `{subindex.title()} PEPs <{target}>`_")
                self.emit_newline()
            self.emit_newline()

            self.emit_title("API")
            self.emit_text(
                "The `PEPS API </api/peps.json>`__ is a JSON file of metadata about "
                "all the published PEPs. :doc:`Read more here <api/index>`."
            )
            self.emit_newline()

        # PEPs by number
        if is_pep0:
            self.emit_title("Numerical Index")
            self.emit_text(
                "The :doc:`numerical index </numerical>` contains "
                "a table of all PEPs, ordered by number."
            )
            self.emit_newline()

        # PEPs by category
        self.emit_title("Index by Category")
        meta, info, provisional, accepted, open_, finished, historical, deferred, dead = _classify_peps(peps)
        pep_categories = [
            ("Process and Meta-PEPs", meta),
            ("Other Informational PEPs", info),
            ("Provisional PEPs (provisionally accepted; interface may still change)", provisional),
            ("Accepted PEPs (accepted; may not be implemented yet)", accepted),
            ("Open PEPs (under consideration)", open_),
            ("Finished PEPs (done, with a stable interface)", finished),
            ("Historical Meta-PEPs and Informational PEPs", historical),
            ("Deferred PEPs (postponed pending further research or updates)", deferred),
            ("Rejected, Superseded, and Withdrawn PEPs", dead),
        ]
        for (category, peps_in_category) in pep_categories:
            # For sub-indices, only emit categories with entries.
            # For PEP 0, emit every category, but only with a table when it has entries.
            if len(peps_in_category) > 0:
                self.emit_pep_category(category, peps_in_category)
            elif is_pep0:
                # emit the category with no table
                self.emit_subtitle(category)
                self.emit_text("None.")
                self.emit_newline()

        self.emit_newline()

        # Reserved PEP numbers
        if is_pep0:
            self.emit_title("Reserved PEP Numbers")
            self.emit_column_headers(include_version=False)
            for number, claimants in sorted(self.RESERVED.items()):
                self.emit_pep_row(
                    shorthand="",
                    number=number,
                    title="RESERVED",
                    authors=claimants,
                    python_version=None,
                )

            self.emit_newline()

        # PEP types key
        self.emit_title("PEP Types Key")
        for type_ in sorted(TYPE_VALUES):
            self.emit_text(
                f"* **{type_[0]}** --- *{type_}*: {ABBREVIATED_TYPES[type_]}"
            )
            self.emit_newline()

        self.emit_text(":pep:`More info in PEP 1 <1#pep-types>`.")
        self.emit_newline()

        # PEP status key
        self.emit_title("PEP Status Key")
        for status in sorted(STATUS_VALUES):
            # Draft PEPs have no status displayed, Active shares a key with Accepted
            status_code = "<No letter>" if status == STATUS_DRAFT else status[0]
            self.emit_text(
                f"* **{status_code}** --- *{status}*: {ABBREVIATED_STATUSES[status]}"
            )
            self.emit_newline()

        self.emit_text(":pep:`More info in PEP 1 <1#pep-review-resolution>`.")
        self.emit_newline()

        if is_pep0:
            # PEP owners
            authors_dict = _verify_email_addresses(peps)
            max_name_len = max(len(author_name) for author_name in authors_dict)
            self.emit_title("Authors/Owners")
            self.emit_author_table_separator(max_name_len)
            self.emit_text(f"{'Name':{max_name_len}}  Email Address")
            self.emit_author_table_separator(max_name_len)
            for author_name in _sort_authors(authors_dict):
                # Use the email from authors_dict instead of the one from "author" as
                # the author instance may have an empty email.
                self.emit_text(f"{author_name:{max_name_len}}  {authors_dict[author_name]}")
            self.emit_author_table_separator(max_name_len)
            self.emit_newline()
            self.emit_newline()

        pep0_string = "\n".join(self.output)
        return pep0_string


def _classify_peps(peps: list[PEP]) -> tuple[list[PEP], ...]:
    """Sort PEPs into meta, informational, accepted, open, finished,
    and essentially dead."""
    meta = []
    info = []
    provisional = []
    accepted = []
    open_ = []
    finished = []
    historical = []
    deferred = []
    dead = []
    for pep in peps:
        # Order of 'if' statement important.  Key Status values take precedence
        # over Type value, and vice-versa.
        if pep.status == STATUS_DRAFT:
            open_.append(pep)
        elif pep.status == STATUS_DEFERRED:
            deferred.append(pep)
        elif pep.pep_type == TYPE_PROCESS:
            if pep.status in {STATUS_ACCEPTED, STATUS_ACTIVE}:
                meta.append(pep)
            elif pep.status in {STATUS_WITHDRAWN, STATUS_REJECTED}:
                dead.append(pep)
            else:
                historical.append(pep)
        elif pep.status in DEAD_STATUSES:
            dead.append(pep)
        elif pep.pep_type == TYPE_INFO:
            # Hack until the conflict between the use of "Final"
            # for both API definition PEPs and other (actually
            # obsolete) PEPs is addressed
            if pep.status == STATUS_ACTIVE or "release schedule" not in pep.title.lower():
                info.append(pep)
            else:
                historical.append(pep)
        elif pep.status == STATUS_PROVISIONAL:
            provisional.append(pep)
        elif pep.status in {STATUS_ACCEPTED, STATUS_ACTIVE}:
            accepted.append(pep)
        elif pep.status == STATUS_FINAL:
            finished.append(pep)
        else:
            raise PEPError(f"Unsorted ({pep.pep_type}/{pep.status})", pep.filename, pep.number)
    return meta, info, provisional, accepted, open_, finished, historical, deferred, dead


def _verify_email_addresses(peps: list[PEP]) -> dict[str, str]:
    authors_dict: dict[str, list[str]] = {}
    for pep in peps:
        for author in pep.authors:
            # If this is the first time we have come across an author, add them.
            if author.full_name not in authors_dict:
                authors_dict[author.full_name] = []

            # If the new email is an empty string, move on.
            if not author.email:
                continue
            # If the email has not been seen, add it to the list.
            emails = authors_dict[author.full_name]
            if author.email not in emails:
                emails.append(author.email)

    # Combine multiple email addresses with commas. Since peps is
    # sorted by PEP number, this should produce a deterministic
    # output.
    return {name: ', '.join(emails) for name, emails in authors_dict.items()}


def _sort_authors(authors_dict: dict[str, str]) -> list[str]:
    return sorted(authors_dict, key=_author_sort_by)


def _author_sort_by(author_name: str) -> str:
    """Skip lower-cased words in surname when sorting."""
    surname, *_ = author_name.split(",")
    surname_parts = surname.split()
    for i, part in enumerate(surname_parts):
        if part[0].isupper():
            base = " ".join(surname_parts[i:]).lower()
            return unicodedata.normalize("NFKD", base)
    # If no capitals, use the whole string
    return unicodedata.normalize("NFKD", surname.lower())
