import re
from copy import deepcopy
from dataclasses import Field, asdict, dataclass, field, fields
from datetime import datetime
from typing import Generator, Literal

from dateutil.parser import parse

from epstein_files.util.constant.names import *
from epstein_files.util.constant.strings import *
from epstein_files.util.data import without_nones

DuplicateType = Literal['earlier', 'quoted', 'redacted', 'same']
Metadata = dict[str, bool | datetime | int | str | list[str | None] |dict[str, bool | str]]

# Misc
CONSTANTIZE_NAMES = False  # A flag set to True that causes repr() of these classes to return strings of usable code
INDENT = '    '
INDENT_NEWLINE = f'\n{INDENT}'
INDENTED_JOIN = f',{INDENT_NEWLINE}'
MAX_LINE_LENGTH = 150
REPUTATION_MGMT = f'{REPUTATION} management'
SAME = 'same'

DUPE_TYPE_STRS: dict[DuplicateType, str] = {
    'earlier': 'an earlier draft of',
    'quoted': 'quoted in full in',
    'redacted': 'a redacted version of',
    SAME: 'the same as',
}

FIELD_SORT_KEY = {
    'id': 'a',
    'author': 'aa',
    'attribution_reason': 'zz',
}

FINANCIAL_REPORTS_AUTHORS = [
    BOFA,
    DEUTSCHE_BANK,
    ELECTRON_CAPITAL_PARTNERS,
    GOLDMAN_INVESTMENT_MGMT,
    'Invesco',
    JP_MORGAN,
    'Morgan Stanley',
    'S&P',
]

# Fields like timestamp and author are better added from the Document object
INVALID_FOR_METADATA = [
    'actual_text',
    'date',
    'id',
    'timestamp',
    'was_generated',
]


@dataclass(kw_only=True)
class DocCfg:
    """
    Encapsulates info about files that needs to be manually configured because it cannot be programmatically inferred.

    Attributes:
        id (str): ID of file
        author (str | None): Author of the document (if any)
        category (str | None): Type of file
        date (str | None): If passed will be immediated parsed into the 'timestamp' field
        dupe_of_id (str | None): If this is a dupe the ID of the duplicated file. This file will be suppressed
        dupe_type (DuplicateType | None): The type of duplicate this file is or its 'duplicate_ids' are
        duplicate_ids (list[str]): Inverse of 'dupe_of_id' - this file will NOT be suppressed but 'duplicate_ids' will be
        is_interesting (bool): Override other considerations and always consider this file interesting
        timestamp (datetime | None): Time this email was sent, file was created, article published, etc.
        was_generated (bool): True if this object was generated by the duplicate_cfgs() method
    """
    id: str
    author: str | None = None
    category: str | None = None
    date: str | None = None
    description: str | None = None
    dupe_of_id: str | None = None
    dupe_type: DuplicateType | None = None
    duplicate_ids: list[str] = field(default_factory=list)
    is_interesting: bool = False
    timestamp: datetime | None = None
    was_generated: bool = False

    def __post_init__(self):
        if self.date:
            self.timestamp = parse(self.date)

        if self.dupe_of_id or self.duplicate_ids:
            self.dupe_type = self.dupe_type or SAME

    def duplicate_reason(self) -> str | None:
        if self.dupe_type is not None:
            return DUPE_TYPE_STRS[self.dupe_type]

    def duplicate_cfgs(self) -> Generator['DocCfg', None, None]:
        """Create synthetic DocCfg objects that set the 'dupe_of_id' field to point back to this object."""
        for id in self.duplicate_ids:
            dupe_cfg = deepcopy(self)
            dupe_cfg.id = id
            dupe_cfg.dupe_of_id = self.id
            dupe_cfg.duplicate_ids = []
            dupe_cfg.dupe_type = self.dupe_type
            dupe_cfg.was_generated = True
            yield dupe_cfg

    def info_str(self) -> str | None:
        """String that summarizes what is known about this document."""
        if self.category == REPUTATION:
            return f"{REPUTATION_MGMT}: {self.description}"
        elif self.author and self.description:
            if self.category in [ACADEMIA, BOOK]:
                return self.title_by_author()
            elif self.category == FINANCE and self.author in FINANCIAL_REPORTS_AUTHORS:
                return f"{self.author} report: '{self.description}'"
        elif self.category and self.author is None and self.description is None:
            return self.category

        pieces = without_nones([self.author, self.description])
        return ' '.join(pieces) if pieces else None

    def metadata(self) -> Metadata:
        non_null_fields = {k: v for k, v in asdict(self).items() if v and k not in INVALID_FOR_METADATA}

        if self.category in [EMAIL, TEXT_MESSAGE]:
            del non_null_fields['category']

        return non_null_fields

    def non_null_field_names(self) -> list[str]:
        return [f.name for f in self.sorted_fields() if getattr(self, f.name)]

    def sorted_fields(self) -> list[Field]:
        return sorted(fields(self), key=lambda f: FIELD_SORT_KEY.get(f.name, f.name))

    def title_by_author(self) -> str:
        if not (self.author and self.description):
            raise RuntimeError(f"Can't call title_by_author() without author and description!")

        title = self.description if '"' in self.description else f"'{self.description}'"
        return f"{title} by {self.author}"

    def _props_strs(self) -> list[str]:
        props = []
        add_prop = lambda f, value: props.append(f"{f.name}={value}")

        for _field in self.sorted_fields():
            value = getattr(self, _field.name)

            if value is None or value is False or (isinstance(value, list) and len(value) == 0):
                continue
            elif _field.name == AUTHOR:
                add_prop(_field, constantize_name(str(value)) if CONSTANTIZE_NAMES else f"'{value}'")
            elif _field.name == 'category' and value in [EMAIL, TEXT_MESSAGE]:
                continue
            elif _field.name == 'recipients' and isinstance(value, list):
                recipients_str = str([constantize_name(r) if (CONSTANTIZE_NAMES and r) else r for r in value])
                add_prop(_field, recipients_str.replace("'", '') if CONSTANTIZE_NAMES else recipients_str)
            elif _field.name == 'timestamp' and self.date is not None:
                continue  # Don't print both timestamp and date
            elif isinstance(value, datetime):
                value_str = re.sub(' 00:00:00', '', str(value))
                add_prop(_field, f"parse('{value_str}')" if CONSTANTIZE_NAMES else f"'{value}'")
            elif isinstance(value, str):
                if "'" in value:
                    value = '"' + value.replace('"', r'\"') + '"'
                else:
                    value = "'" + value.replace("'", r'\'') + "'"

                add_prop(_field, value)
            else:
                add_prop(_field, str(value))

        return props

    def __eq__(self, other: 'DocCfg') -> bool:
        """Return True if everything matches other than the two 'dupe_' fields ('duplicate_ids' is compared)."""
        for _field in self.sorted_fields():
            if _field.name == 'id' or _field.name.startswith('dupe'):
                continue
            elif getattr(self, _field.name) != getattr(other, _field.name):
                return False

        return True

    def __repr__(self) -> str:
        props = self._props_strs()
        type_str = f"{type(self).__name__}("
        single_line_repr = type_str + ', '.join(props) + f')'

        if len(single_line_repr) < MAX_LINE_LENGTH:
            repr_str = single_line_repr
        else:
            repr_str = f"{type_str}{INDENT_NEWLINE}" + INDENTED_JOIN.join(props)
            repr_str += ',' if props else ''
            repr_str += '\n)'

        if CONSTANTIZE_NAMES:
            repr_str = INDENT + INDENT_NEWLINE.join(repr_str.split('\n'))
            return repr_str.replace(',,', ',').replace(',),', '),').replace(',),', '),')
        else:
            return repr_str


@dataclass(kw_only=True)
class CommunicationCfg(DocCfg):
    """
    Manual config is always required for MessengerLog author attribution. It's also often needed for Email
    files to handle the terrible OCR text that Congress provided which messes up a lot of the email headers.

    Attributes:
        attribution_reason (str | None): Optional explanation of why this email was attributed to this author.
        is_attribution_uncertain (bool): True if we have a good idea of who the author is but are not 100% certain
    """
    attribution_reason: str | None = None
    is_attribution_uncertain: bool = False

    def __repr__(self) -> str:
        return super().__repr__()


@dataclass(kw_only=True)
class EmailCfg(CommunicationCfg):
    """
    Attributes:
        actual_text (str | None): In dire cases of broken OCR we just configure the body of the email as a string.
        is_fwded_article (bool): True if this is a newspaper article someone fwded. Used to exclude articles from word counting.
        recipients (list[str | None]): Who received the email
    """
    actual_text: str | None = None  # Override for the Email._actual_text() method for particularly broken emails
    is_fwded_article: bool = False
    recipients: list[str | None] = field(default_factory=list)

    def __post_init__(self):
        super().__post_init__()
        self.category = EMAIL

    @classmethod
    def from_doc_cfg(cls, cfg: DocCfg) -> 'EmailCfg':
        return cls(**asdict(cfg))

    # This is necessary for some dumb reason. @dataclass(repr=False) doesn't cut it
    def __repr__(self) -> str:
        return super().__repr__()


@dataclass(kw_only=True)
class TextCfg(CommunicationCfg):
    def __post_init__(self):
        super().__post_init__()
        self.category = TEXT_MESSAGE

    # This is necessary for some dumb reason. @dataclass(repr=False) doesn't cut it
    def __repr__(self) -> str:
        return super().__repr__()
