import sys
from abc import abstractmethod, ABC
from datetime import datetime, timezone
from hashlib import sha1
from typing import Generic, TypeVar, Union, TYPE_CHECKING

import mwreverts
import mwtypes
import mwxml

import pyarrow as pa

if TYPE_CHECKING:
    from wikiq.wikitext_parser import WikitextParser

T = TypeVar('T')


class RevisionField(ABC, Generic[T]):
    """
    Abstract type which represents a field in a table of page revisions.
    """

    @property
    @abstractmethod
    def field(self) -> pa.Field:
        pass

    @abstractmethod
    def extract(self, page: mwtypes.Page, revisions: list[mwxml.Revision]) -> T:
        """
        :param page: The page for this set of revisions.
        :param revisions: The set of revisions to compute the field from.
        Revisions are passed in chronological order, so use revisions[-1] to
        access the most recent revision in the set.

        Implementations of extract should handle the case where revisions is
        either a single revision (collapse-user=FALSE), or a full edit session
        of contiguous edits by the same user (collapse-user=TRUE).
        """
        pass


class RevisionTable:
    columns: list[RevisionField]

    def __init__(self, columns: list[RevisionField]):
        self.columns = columns

    def schema(self) -> pa.Schema:
        return pa.schema([c.field for c in self.columns])

    def extract_row(self, page: mwtypes.Page, revisions: list[mwxml.Revision]) -> dict:
        """Extract a single row dict for the given page and revisions."""
        return {
            column.field.name: column.extract(page, revisions)
            for column in self.columns
        }


class RevisionId(RevisionField[int]):
    field = pa.field("revid", pa.int64())

    def extract(self, _: mwtypes.Page, revisions: list[mwxml.Revision]) -> int:
        revision = revisions[-1]
        return revision.id


class RevisionTimestamp(RevisionField[datetime]):
    field = pa.field("date_time", pa.timestamp('s'))

    def extract(self, page: mwtypes.Page, revisions: list[mwxml.Revision]) -> datetime:
        revision = revisions[-1]
        return revision.timestamp


class RevisionArticleId(RevisionField[int]):
    field = pa.field("articleid", pa.int64())

    def extract(self, page: mwtypes.Page, revisions: list[mwxml.Revision]) -> int:
        return page.id


class RevisionEditorId(RevisionField[Union[int, None]]):
    field = pa.field("editorid", pa.int64(), nullable=True)

    def extract(self, page: mwtypes.Page, revisions: list[mwxml.Revision]) -> Union[int, None]:
        revision = revisions[-1]
        if revision.deleted.user:
            return None

        return revision.user.id


class RevisionEditSummary(RevisionField[Union[str, None]]):
    field = pa.field("edit_summary", pa.string(), nullable=True)

    def extract(self, page: mwtypes.Page, revisions: list[mwxml.Revision]) -> Union[str, None]:
        revision = revisions[-1]

        return revision.comment

class RevisionIsAnon(RevisionField[Union[bool, None]]):
    field = pa.field("anon", pa.bool_(), nullable=True)

    def extract(self, page: mwtypes.Page, revisions: list[mwxml.Revision]) -> Union[bool, None]:
        revision = revisions[-1]
        if revision.deleted.user:
            return None

        return revision.user.id is None


class RevisionEditorText(RevisionField[Union[str, None]]):
    field = pa.field("editor", pa.string(), nullable=True)

    def extract(self, page: mwtypes.Page, revisions: list[mwxml.Revision]) -> Union[str, None]:
        revision = revisions[-1]
        if revision.deleted.user:
            return None

        return revision.user.text


class RevisionPageTitle(RevisionField[str]):
    field = pa.field("title", pa.string())

    def extract(self, page: mwtypes.Page, revisions: list[mwxml.Revision]) -> str:
        return page.title


class RevisionDeleted(RevisionField[bool]):
    field = pa.field("deleted", pa.bool_())

    def extract(self, page: mwtypes.Page, revisions: list[mwxml.Revision]) -> bool:
        revision = revisions[-1]
        return revision.deleted.text


class RevisionNamespace(RevisionField[int]):
    field = pa.field("namespace", pa.int32())

    def extract(self, page: mwtypes.Page, revisions: list[mwxml.Revision]) -> int:
        return page.namespace


class RevisionSha1(RevisionField[str]):
    field = pa.field("sha1", pa.string())

    def extract(self, page: mwtypes.Page, revisions: list[mwxml.Revision]) -> str:
        revision = revisions[-1]
        return revision.sha1


class RevisionTextChars(RevisionField[Union[int, None]]):
    field = pa.field("text_chars", pa.int32(), nullable=True)

    def extract(self, page: mwtypes.Page, revisions: list[mwxml.Revision]) -> Union[int, None]:
        revision = revisions[-1]
        if not revision.deleted.text:
            return len(revision.text)

        return None

class RevisionIsMinor(RevisionField[bool]):
    field = pa.field("minor", pa.bool_())

    def extract(self, page: mwtypes.Page, revisions: list[mwxml.Revision]) -> bool:
        revision = revisions[-1]
        return revision.minor


class RevisionReverts(RevisionField[Union[str, None]]):
    def __init__(self):
        super().__init__()
        self.rev_detector: Union[mwreverts.Detector, None] = None

    field = pa.field("reverteds", pa.string(), nullable=True)

    def extract(self, page: mwtypes.Page, revisions: list[mwxml.Revision]) -> Union[str, None]:
        if self.rev_detector is None:
            return None

        revision = revisions[-1]
        if revision.deleted.text:
            return None

        revert = self.rev_detector.process(revision.sha1, revision.id)
        if revert is None:
            return None

        return ",".join([str(s) for s in revert.reverteds])


class RevisionCollapsed(RevisionField[int]):
    field = pa.field("collapsed_revs", pa.int64())

    def extract(self, page: mwtypes.Page, revisions: list[mwxml.Revision]) -> int:
        return len(revisions)


class RevisionText(RevisionField[str]):
    field = pa.field("text", pa.string(), nullable=False)

    def extract(self, page: mwtypes.Page, revisions: list[mwxml.Revision]) -> str:
        revision = revisions[-1]
        return revision.text


class RevisionExternalLinks(RevisionField[Union[list[str], None]]):
    """Extract all external links from revision text."""

    field = pa.field("external_links", pa.list_(pa.string()), nullable=True)

    def __init__(self, wikitext_parser: "WikitextParser"):
        super().__init__()
        self.wikitext_parser = wikitext_parser

    def extract(self, page: mwtypes.Page, revisions: list[mwxml.Revision]) -> Union[list[str], None]:
        revision = revisions[-1]
        if revision.deleted.text:
            return None
        return self.wikitext_parser.extract_external_links(revision.text)


class RevisionCitations(RevisionField[Union[list[str], None]]):
    """Extract citations from ref tags and cite templates."""

    field = pa.field("citations", pa.list_(pa.string()), nullable=True)

    def __init__(self, wikitext_parser: "WikitextParser"):
        super().__init__()
        self.wikitext_parser = wikitext_parser

    def extract(self, page: mwtypes.Page, revisions: list[mwxml.Revision]) -> Union[list[str], None]:
        revision = revisions[-1]
        if revision.deleted.text:
            return None
        return self.wikitext_parser.extract_citations(revision.text)


class RevisionWikilinks(RevisionField[Union[list[dict], None]]):
    """Extract all internal wikilinks from revision text."""

    # Struct type with title and optional display text
    field = pa.field("wikilinks", pa.list_(pa.struct([
        pa.field("title", pa.string()),
        pa.field("text", pa.string(), nullable=True),
    ])), nullable=True)

    def __init__(self, wikitext_parser: "WikitextParser"):
        super().__init__()
        self.wikitext_parser = wikitext_parser

    def extract(self, page: mwtypes.Page, revisions: list[mwxml.Revision]) -> Union[list[dict], None]:
        revision = revisions[-1]
        if revision.deleted.text:
            return None
        return self.wikitext_parser.extract_wikilinks(revision.text)


class RevisionTemplates(RevisionField[Union[list[dict], None]]):
    """Extract all templates from revision text."""

    # Struct type with name and params map
    field = pa.field("templates", pa.list_(pa.struct([
        pa.field("name", pa.string()),
        pa.field("params", pa.map_(pa.string(), pa.string())),
    ])), nullable=True)

    def __init__(self, wikitext_parser: "WikitextParser"):
        super().__init__()
        self.wikitext_parser = wikitext_parser

    def extract(self, page: mwtypes.Page, revisions: list[mwxml.Revision]) -> Union[list[dict], None]:
        revision = revisions[-1]
        if revision.deleted.text:
            return None
        return self.wikitext_parser.extract_templates(revision.text)


class RevisionHeadings(RevisionField[Union[list[dict], None]]):
    """Extract all section headings from revision text."""

    # Struct type with level and text
    field = pa.field("headings", pa.list_(pa.struct([
        pa.field("level", pa.int8()),
        pa.field("text", pa.string()),
    ])), nullable=True)

    def __init__(self, wikitext_parser: "WikitextParser"):
        super().__init__()
        self.wikitext_parser = wikitext_parser

    def extract(self, page: mwtypes.Page, revisions: list[mwxml.Revision]) -> Union[list[dict], None]:
        revision = revisions[-1]
        if revision.deleted.text:
            return None
        return self.wikitext_parser.extract_headings(revision.text)


class RevisionParserTimeout(RevisionField[bool]):
    """Track whether the wikitext parser timed out for this revision."""

    field = pa.field("parser_timeout", pa.bool_())

    def __init__(self, wikitext_parser: "WikitextParser"):
        super().__init__()
        self.wikitext_parser = wikitext_parser

    def extract(self, page: mwtypes.Page, revisions: list[mwxml.Revision]) -> bool:
        return self.wikitext_parser.last_parse_timed_out
