import re
import datetime
import html as _html_module
from typing import Dict, List, Optional, Tuple

_STARS_RE    = re.compile(r'class="stars\s+stars-(\d)"')
_HTML_TAG_RE = re.compile(r'<[^>]+>')


def _preprocess_new_format(raw: str):
    """If raw looks like HTML, extract star ratings and return (plain_text, star_list).
    Otherwise return (raw, []) unchanged."""
    if '<' not in raw:
        return raw, []
    stars = [int(m) for m in _STARS_RE.findall(raw)]
    # Strip tags and unescape HTML entities to get equivalent plain text
    plain = _HTML_TAG_RE.sub('', raw)
    plain = _html_module.unescape(plain)
    return plain, stars

# (locale_code, date_fmt, detection_token)
# date_fmt: 'mdy' = MM/DD/YYYY, 'dmy' = DD/MM/YYYY, 'dmy_dot' = DD.MM.YYYY
_LOCALE_MAP = [
    ('en', 'mdy',     'Posted on '),
    ('es', 'dmy',     'Opinión publicada el '),
    ('fr', 'dmy',     'Avis déposé le '),
    ('it', 'dmy',     'Recensione inserita il '),
    ('pl', 'dmy_dot', 'Opinia złożona w dniu '),
]

_ORDER_RE     = re.compile(r'#(\d+)calendar_today')
_PURCHASE_RE  = re.compile(r'(\d{4}-\d{2}-\d{2})')
_DATE_SLASH   = re.compile(r'\d{2}/\d{2}/\d{4}')
_DATE_DOT     = re.compile(r'\d{2}\.\d{2}\.\d{4}')
_REPLY_DATE   = re.compile(r'(\d{2}/\d{2}/\d{4})')  # always MM/DD regardless of locale


def _detect_locale(text: str) -> Tuple[str, str]:
    for code, fmt, token in _LOCALE_MAP:
        if token in text:
            return code, fmt
    return 'unknown', 'dmy'


def _parse_date(date_str: str, fmt: str) -> Optional[datetime.date]:
    try:
        sep = '.' if fmt == 'dmy_dot' else '/'
        a, b, c = date_str.split(sep)
        if fmt == 'mdy':
            return datetime.date(int(c), int(a), int(b))
        else:
            return datetime.date(int(c), int(b), int(a))
    except (ValueError, AttributeError):
        return None


def _parse_reply_date(date_str: str) -> Optional[datetime.date]:
    try:
        m, d, y = date_str.split('/')
        return datetime.date(int(y), int(m), int(d))
    except (ValueError, AttributeError):
        return None


def _isodate(d: Optional[datetime.date]) -> Optional[str]:
    return d.isoformat() if d else None


def parse_new_format(raw: str, star_list: Optional[List[int]] = None) -> List[Dict]:
    raw = raw.replace('\r\n', '\n').replace('\r', '\n')
    # Fall back to HTML extraction only if no star_list was provided by the caller
    if star_list is None:
        raw, star_list = _preprocess_new_format(raw)
    else:
        star_list = star_list or []
    locale_code, date_fmt = _detect_locale(raw)

    # Blocks are delimited by 'info' on its own line
    blocks = re.split(r'\ninfo(?:\n|$)', raw)

    results = []
    for block in blocks:
        block = block.strip()
        if not block:
            continue

        lines = block.split('\n')
        if not lines:
            continue

        # --- order ID ---
        order_match = _ORDER_RE.search(lines[0])
        if not order_match:
            continue
        order_id = order_match.group(1)

        # --- purchase date (always ISO on line 0) ---
        purchase_match = _PURCHASE_RE.search(lines[0])
        purchase_date = (
            datetime.date.fromisoformat(purchase_match.group(1))
            if purchase_match else None
        )

        # --- customer name (line 1) ---
        customer_name = lines[1].strip() if len(lines) > 1 else ''

        # --- review date: first line after line 1 that contains a date pattern ---
        review_date: Optional[datetime.date] = None
        review_date_idx: Optional[int] = None
        for i, line in enumerate(lines):
            if i < 2:
                continue
            m = _DATE_DOT.search(line) or _DATE_SLASH.search(line)
            if m:
                review_date = _parse_date(m.group(0), date_fmt)
                review_date_idx = i
                break

        # --- review text: non-empty lines immediately after date line ---
        review_text = ''
        if review_date_idx is not None:
            text_lines = []
            for line in lines[review_date_idx + 1:]:
                if line.strip() == '':
                    break
                text_lines.append(line.strip())
            review_text = '\n'.join(text_lines)

        # --- seller reply: blocks with a reply have NO 'send' token ---
        has_reply = not bool(re.search(r'\nsend\S', '\n' + block))

        # --- reply date (always MM/DD/YYYY) ---
        seller_reply_date: Optional[datetime.date] = None
        if has_reply:
            reply_match = _REPLY_DATE.search(block)
            if reply_match:
                seller_reply_date = _parse_reply_date(reply_match.group(1))

        idx = len(results)
        results.append({
            'order_id': order_id,
            'purchase_date': _isodate(purchase_date),
            'customer_name': customer_name,
            'review_date': _isodate(review_date),
            'review_text': review_text,
            'star_rating': star_list[idx] if idx < len(star_list) else None,
            'has_seller_reply': has_reply,
            'seller_reply_date': _isodate(seller_reply_date),
            'locale': locale_code,
        })

    return results


# ── Old format ────────────────────────────────────────────────────────────────

_OLD_BLOCK_EN = re.compile(r'Feedback posted on ')
_OLD_BLOCK_FR = re.compile(r'Avis posté le ')
_OLD_HEADER_EN = re.compile(
    r'Feedback posted on (\d{4}-\d{2}-\d{2}) \d{2}:\d{2}:\d{2} - Order #(\d+) on (\d{4}-\d{2}-\d{2})'
)
_OLD_HEADER_FR = re.compile(
    r'Avis posté le (\d{4}-\d{2}-\d{2}) \d{2}:\d{2}:\d{2} - Commande #(\d+) du (\d{4}-\d{2}-\d{2})'
)
_OLD_SCORE_RE = re.compile(r'(\d(?:\.\d)?)/5')

# "no comment" sentinel values per locale
_NO_COMMENT = {
    'There is no comments',
    'Pas de commentaires',
}


def parse_old_format(raw: str) -> List[Dict]:
    raw = raw.replace('\r\n', '\n').replace('\r', '\n')

    # Detect locale and pick the right header pattern + block splitter
    if _OLD_BLOCK_FR.search(raw):
        locale_code = 'fr'
        header_re = _OLD_HEADER_FR
        comment_label = 'Commentaire:'
        block_pattern = r'(?=Avis posté le )'
    else:
        locale_code = 'en'
        header_re = _OLD_HEADER_EN
        comment_label = 'Comment:'
        block_pattern = r'(?=Feedback posted on )'

    blocks = re.split(block_pattern, raw)

    results = []
    for block in blocks:
        block = block.strip()
        if not block:
            continue

        # --- header: review date + order ID + purchase date ---
        header_match = header_re.search(block)
        if not header_match:
            continue

        review_date = datetime.date.fromisoformat(header_match.group(1))
        order_id = header_match.group(2)
        purchase_date = datetime.date.fromisoformat(header_match.group(3))

        # --- scores: extract all N/5 values in order (module, docs, install, average) ---
        scores = _OLD_SCORE_RE.findall(block)
        score_module  = float(scores[0]) if len(scores) > 0 else None
        score_docs    = float(scores[1]) if len(scores) > 1 else None
        score_install = float(scores[2]) if len(scores) > 2 else None
        score_average = float(scores[3]) if len(scores) > 3 else None

        # --- comment: everything after the comment label ---
        comment_text = None
        if comment_label in block:
            after = block.split(comment_label, 1)[1].strip()
            if after and after not in _NO_COMMENT:
                comment_text = after

        # --- skill level: line after the skill label ---
        skill_level = None
        for label in ('Skill level:', 'Niveau de compétence:'):
            if label in block:
                idx = block.index(label) + len(label)
                rest = block[idx:].lstrip('\n').split('\n')[0].strip()
                if rest:
                    skill_level = rest
                break

        results.append({
            'order_id': order_id,
            'purchase_date': _isodate(purchase_date),
            'review_date': _isodate(review_date),
            'score_module': score_module,
            'score_docs': score_docs,
            'score_install': score_install,
            'score_average': score_average,
            'skill_level': skill_level,
            'comment_text': comment_text,
            'locale': locale_code,
        })

    return results
