# backend/app/connectors/mail/normalize.py
# provider(mock/gmail/imap/outlook) → 내부 email 모델(phase-9) 정규화 + 멱등 upsert.
import base64
import hashlib
import html as html_lib
import re
import uuid
from datetime import UTC, datetime
from email.header import decode_header, make_header
from email.utils import parseaddr

from sqlmodel import Session, select

from ...models import AccountFolder, ConnectorAccount, Email, ExternalLink
from ..base import NormalizedRecord, RawRecord

# connector_account id ↔ mail_account id 매핑(시드 정합)
_ACCOUNT_KEY = {
    "ca-mail-work": "work",
    "ca-mail-personal": "personal",
    "ca-mail-side": "side",
}


def mail_account_id_for(email: str) -> str:
    """이메일 주소 → 안정적 MailAccount id 슬러그(phase-16 멀티계정)."""
    return "ma-" + hashlib.sha1(email.lower().encode()).hexdigest()[:8]


def _name_from_addr(addr: str) -> str:
    """표시 이름이 없을 때 도메인에서 추정(noreply@e.coupang.com → Coupang)."""
    if "@" not in (addr or ""):
        return ""
    domain = addr.rsplit("@", 1)[1].lower().strip()
    labels = [x for x in domain.split(".") if x]
    if not labels:
        return ""
    core = labels[-2] if len(labels) >= 2 else labels[0]  # 등록가능 라벨(SLD)
    return core[:1].upper() + core[1:]


def _decode_mime_words(s: str) -> str:
    """RFC2047 인코딩 단어(=?utf-8?b?...?=) 디코딩 — Gmail 헤더의 비ASCII 이름 복원."""
    if not s or "=?" not in s:
        return s
    try:
        return str(make_header(decode_header(s)))
    except Exception:
        return s


def sender_parts(raw: str) -> tuple[str, str]:
    """보낸사람 원문('이름 <메일>' 또는 '메일') → (표시이름, 메일주소).
    인코딩 단어는 디코딩, 표시이름 없으면 도메인 추정, 그것도 없으면 원문을 이름으로."""
    name, addr = parseaddr(raw or "")
    name = _decode_mime_words((name or "").strip().strip('"').strip())
    addr = (addr or "").strip()
    if not addr and "@" in (raw or ""):
        addr = (raw or "").strip()
    disp = name or _name_from_addr(addr) or _decode_mime_words((raw or "").strip())
    return disp, addr


def _outlook_from(frm: dict) -> str:
    """Outlook 발신자 → '이름 <메일>'(유니코드 그대로 보존). 이름 없으면 주소만."""
    name = (frm.get("name") or "").strip().replace("<", "").replace(">", "")
    addr = (frm.get("address") or "").strip()
    if name and addr and name.lower() != addr.lower():
        if "," in name or '"' in name:  # parseaddr 가 콤마로 안 쪼개도록 인용
            name = '"' + name.replace('"', "") + '"'
        return f"{name} <{addr}>"
    return addr or name


def _mail_account_for(account: ConnectorAccount, fallback: str = "work") -> str:
    """connector 계정 → 내부 mail_account id. 시드 계정은 정적 매핑, real 계정은 이메일 슬러그."""
    if account.id in _ACCOUNT_KEY:
        return _ACCOUNT_KEY[account.id]
    if getattr(account, "external_account_id", ""):
        return mail_account_id_for(account.external_account_id)
    return fallback


def _gmail_received(p: dict) -> datetime | None:
    """Gmail internalDate(epoch ms) → 수신 시각(UTC aware). 정렬·표시 기준."""
    ms = p.get("internalDate")
    if not ms:
        return None
    try:
        return datetime.fromtimestamp(int(ms) / 1000, tz=UTC)
    except (ValueError, TypeError):
        return None


def _parse_iso_dt(s: str) -> datetime | None:
    """Outlook receivedDateTime(ISO8601, 'Z') → 수신 시각(UTC aware)."""
    if not s:
        return None
    try:
        dt = datetime.fromisoformat(s.replace("Z", "+00:00"))
        return dt if dt.tzinfo else dt.replace(tzinfo=UTC)
    except ValueError:
        return None


# HTML 주석/조건부 주석(<!--[if mso]>..<![endif]-->, <!--[if false]><!-->) — 멀티라인.
# 평문 파트에 섞여 들어오는 MSO/VML 블록을 통째로 제거(DOTALL).
_HTML_COMMENT = re.compile(r"(?is)<!--.*?-->")
_DROP_TAGS = re.compile(r"(?is)<\s*(script|style|head)[^>]*>.*?<\s*/\s*\1\s*>")
_BR = re.compile(r"(?i)<\s*br\s*/?\s*>")
_BLOCK_END = re.compile(r"(?is)</\s*(p|div|tr|li|h[1-6]|blockquote|table|ul|ol)\s*>")
_TAG = re.compile(r"<[^>]+>")
# 앵글로 감싼 URL/이메일(<https://..>, <tel:..>, <a@b.com>) → 태그 오인 말고 언랩(링크 보존).
_ANGLE_URL = re.compile(r"<\s*((?:https?|mailto|tel):[^>\s]+)\s*>")
_ANGLE_EMAIL = re.compile(r"<\s*([^<>\s@]+@[^<>\s]+)\s*>")
# URL/이메일 언랩 후 남는 실제 HTML 태그(<v:roundrect ..>, <div>, </p> 등) 제거.
_STRAY_TAG = re.compile(r"</?\s*[a-zA-Z!][\w:.-]*(?:\s[^>]*)?/?>")
# &NBSP; 처럼 대소문자가 어긋난 엔티티도 디코딩(html.unescape 는 케이스 민감).
_ENTITY_RE = re.compile(r"&[A-Za-z][A-Za-z0-9]*;")


def _decode_entity(m: "re.Match") -> str:
    tok = m.group(0)
    out = html_lib.unescape(tok)
    if out != tok:  # 정상 케이스(&amp; &Aacute; 등)는 그대로 보존
        return out
    low = html_lib.unescape(tok.lower())  # &NBSP; → &nbsp; → 공백
    return low if low != tok.lower() else tok


def _unescape_html(s: str) -> str:
    """명명 엔티티(케이스 보정) + 숫자 엔티티(&#39; 등) 디코딩."""
    return html_lib.unescape(_ENTITY_RE.sub(_decode_entity, s))


# 마케팅/평문 메일에 인라인된 수백자짜리 트래킹 URL → scheme://host/… 로 축약(가독성).
_URL_RUN = re.compile(r"https?://\S+")


def _shorten_url(m: "re.Match") -> str:
    u = m.group(0)
    if len(u) <= 90:  # 평범한 길이는 그대로 둠
        return u
    host = re.match(r"(https?)://([^/?#\s]+)", u)
    return f"{host.group(1)}://{host.group(2)}/…" if host else u[:80] + "…"
# 평문 파트인데 실제론 HTML 소스인 (잘못 만든) 메일 감지 → 태그 제거 처리.
_HTML_HINT = re.compile(
    r"<\s*(html|body|head|table|div|p|br|a|td|tr|span|img|ul|ol|li|font)\b", re.I
)


def _looks_html(text: str) -> bool:
    return bool(_HTML_HINT.search(text or ""))


def _clean_plain_lines(text: str) -> list:
    """평문 파트 정리: HTML 주석·조건부 주석/VML·잔여 태그 제거, <url>/<email> 언랩,
    엔티티 디코딩, CSS 잔재 줄 제거. text/plain 인데 HTML 조각이 섞인 메일에 대응."""
    if not text:
        return []
    s = _HTML_COMMENT.sub(" ", text)          # <!--[if mso]>..<![endif]--> 블록 제거
    s = _ANGLE_URL.sub(r"\1 ", s)              # <https://..> → https://..
    s = _ANGLE_EMAIL.sub(r"\1 ", s)           # <a@b.com> → a@b.com
    s = _STRAY_TAG.sub("", s)                  # 남은 <v:..>/<div> 등 태그 제거
    s = _unescape_html(s)
    s = _URL_RUN.sub(_shorten_url, s)          # 초장문 트래킹 URL 축약
    out = []
    for ln in s.split("\n"):
        ln = re.sub(r"[ \t \xa0]+", " ", ln).strip()
        if ln and not _is_css_noise(ln):
            out.append(ln)
    return out[:400]


def _is_css_noise(ln: str) -> bool:
    """마케팅 메일에서 새어 나온 CSS 잔재 줄 감지(본문 가독성 향상)."""
    low = ln.lower()
    if "mso-" in low or "{" in ln and ("}" in ln or ":" in ln or ";" in ln):
        return True
    stripped = low.lstrip()
    return stripped.startswith(("@media", "@font-face", "@import", "@keyframes"))


def _html_to_lines(html: str) -> list:
    """HTML 본문 → 읽을 수 있는 평문 줄 목록(블록 단위 줄바꿈 보존·엔티티 디코딩)."""
    if not html:
        return []
    s = _HTML_COMMENT.sub(" ", html)  # 조건부 주석/MSO 블록 먼저 제거(멀티라인)
    s = _DROP_TAGS.sub(" ", s)
    s = _BR.sub("\n", s)
    s = _BLOCK_END.sub("\n", s)
    s = _TAG.sub("", s)
    s = _unescape_html(s)
    s = _URL_RUN.sub(_shorten_url, s)  # 초장문 트래킹 URL 축약
    out = []
    for ln in s.split("\n"):
        ln = re.sub(r"[ \t \xa0]+", " ", ln).strip()
        if ln and not _is_css_noise(ln):
            out.append(ln)
    # 과도하게 긴 마케팅 메일도 합리적 길이로 컷(렌더 안정).
    return out[:400]
# Gmail labelId → 내부 한국어 라벨
_LABEL_MAP = {
    "CATEGORY_PERSONAL": "개인",
    "CATEGORY_PROMOTIONS": "프로모션",
    "CATEGORY_UPDATES": "업데이트",
    "CATEGORY_FORUMS": "포럼",
    "CATEGORY_SOCIAL": "소셜",
    "IMPORTANT": "중요",
}


def _gmail_headers(p: dict) -> dict:
    return {h["name"].lower(): h["value"] for h in p.get("payload", {}).get("headers", [])}


def _decode_b64url(data: str) -> str:
    try:
        return base64.urlsafe_b64decode(data + "===").decode("utf-8", "ignore")
    except Exception:
        return ""


def _find_mime_part(part: dict, target: str) -> dict | None:
    """payload 트리에서 target(mimeType) 파트를 재귀 탐색(데이터 있는 것만). 중첩 multipart 대응."""
    if part.get("mimeType", "").startswith(target) and part.get("body", {}).get("data"):
        return part
    for sub in part.get("parts", []) or []:
        found = _find_mime_part(sub, target)
        if found:
            return found
    return None


# 원본 HTML 보존 시 제거할 위험 요소(샌드박스 iframe 가 1차 방어, 이건 심층 방어).
# <script>/<iframe>/<object>/<embed> 블록 + on* 이벤트 핸들러 + javascript: 스킴.
_SCRIPTISH = re.compile(
    r"(?is)<\s*(script|iframe|object|embed|noscript)\b.*?<\s*/\s*\1\s*>"
)
_SELF_SCRIPTISH = re.compile(r"(?is)<\s*(script|iframe|object|embed)\b[^>]*/?>")
_ON_ATTR = re.compile(r"(?is)\son[a-z]+\s*=\s*(\"[^\"]*\"|'[^']*'|[^\s>]+)")
_JS_URL = re.compile(r"(?i)(href|src|action)\s*=\s*([\"']?)\s*javascript:[^\"'>\s]*")
# 본문 HTML 저장 상한(병적으로 큰 마케팅 메일 방지).
_MAX_HTML = 600_000


def _sanitize_html(html: str) -> str:
    """능동 콘텐츠(script/iframe/on*/javascript:)만 제거. 서식·이미지·스타일은 보존."""
    if not html:
        return ""
    s = _HTML_COMMENT.sub(" ", html)  # 조건부/MSO 주석 제거(렌더 불필요·잡음)
    s = _SCRIPTISH.sub("", s)
    s = _SELF_SCRIPTISH.sub("", s)
    s = _ON_ATTR.sub("", s)
    s = _JS_URL.sub(r'\1=\2#', s)
    return s[:_MAX_HTML]


def _gmail_html(p: dict) -> str:
    """Gmail payload 에서 text/html 파트 원본을 추출(능동 콘텐츠 제거). 없으면 빈 문자열."""
    html_part = _find_mime_part(p.get("payload", {}), "text/html")
    if html_part:
        return _sanitize_html(_decode_b64url(html_part["body"]["data"]))
    return ""


def _part_header(part: dict, name: str) -> str:
    low = name.lower()
    for h in part.get("headers", []) or []:
        if (h.get("name") or "").lower() == low:
            return h.get("value", "")
    return ""


def _gmail_body(p: dict) -> list:
    """본문 추출: text/plain 우선, 없으면 text/html(태그 제거), 그것도 없으면 snippet.
    중첩 multipart(mixed>alternative>...) 를 재귀 탐색한다."""
    payload = p.get("payload", {})
    plain = _find_mime_part(payload, "text/plain")
    if plain:
        text = _decode_b64url(plain["body"]["data"])
        if _looks_html(text):  # 잘못 만든 메일: 평문 파트에 HTML 소스가 들어있음
            lines = _html_to_lines(text)
        else:  # 평문 정리(주석/조건부 주석·VML·잔여 태그·<url> 언랩·CSS 잔재 제거)
            lines = _clean_plain_lines(text)
        if lines:
            return lines[:400]
    html_part = _find_mime_part(payload, "text/html")
    if html_part:
        lines = _html_to_lines(_decode_b64url(html_part["body"]["data"]))
        if lines:
            return lines
    snip = p.get("snippet", "")
    return [snip] if snip else []


def _gmail_labels(label_ids: list) -> list:
    return [_LABEL_MAP[lid] for lid in label_ids if lid in _LABEL_MAP]


def _gmail_folder(label_ids: list) -> str:
    """Gmail labelIds → 시스템 폴더(스팸/휴지통/보낸/받은편지함). 카테고리는 라벨이라 폴더 아님."""
    if "SPAM" in label_ids:
        return "spam"
    if "TRASH" in label_ids:
        return "trash"
    if "INBOX" in label_ids:
        return "inbox"
    if "SENT" in label_ids:
        return "sent"
    return "inbox"


def _gmail_attachments(part: dict, out: list | None = None) -> list:
    """payload 트리를 재귀 탐색해 첨부 메타데이터 수집(일반 첨부 + 인라인 cid 이미지).
    인라인 이미지는 filename 이 없어도 Content-ID 가 있으면 수집(본문 HTML 에서 cid: 로 참조)."""
    out = [] if out is None else out
    fn = part.get("filename") or ""
    body = part.get("body") or {}
    cid = _part_header(part, "Content-ID").strip("<>").strip()
    disp = _part_header(part, "Content-Disposition").lower()
    inline = bool(cid) or "inline" in disp
    if body.get("attachmentId") and (fn or cid):
        out.append(
            {
                "id": body["attachmentId"],
                "name": fn or (cid or "image"),
                "mime": part.get("mimeType", "application/octet-stream"),
                "size": int(body.get("size") or 0),
                "content_id": cid,
                "inline": inline,
            }
        )
    for sub in part.get("parts", []) or []:
        _gmail_attachments(sub, out)
    return out


def _gmail_has_attach(part: dict) -> bool:
    """첨부 존재 여부(파일명 있는 파트). 다운로드 불가(attachmentId 없음)여도 표시는 함."""
    if part.get("filename"):
        return True
    return any(_gmail_has_attach(sub) for sub in part.get("parts", []) or [])


def normalize_email(
    account: ConnectorAccount, raw: RawRecord, provider: str = "mock"
) -> NormalizedRecord:
    p = raw.payload
    if provider == "gmail":
        headers = _gmail_headers(p)
        label_ids = p.get("labelIds", [])
        attachments = _gmail_attachments(p.get("payload", {}))
        fields = {
            "account": _mail_account_for(account),
            "from_key": headers.get("from", ""),
            "to": headers.get("to", "나"),
            "cc": headers.get("cc", ""),
            "thread_id": p.get("threadId", ""),
            "subject": headers.get("subject", "(제목 없음)"),
            "body": _gmail_body(p),
            "body_html": _gmail_html(p),
            # 카테고리(중요/프로모션…) + 사용자 라벨(커넥터가 id→이름 해석해 주입).
            "labels": _gmail_labels(label_ids) + list(p.get("_userLabels", [])),
            "folder": _gmail_folder(label_ids),
            "read": "UNREAD" not in label_ids,
            "starred": "STARRED" in label_ids,
            "has_attach": _gmail_has_attach(p.get("payload", {})),
            "attachments": attachments,
            "preview": p.get("snippet", ""),
            # 표시용 time/date 는 응답 시 received_at 으로 산출(상대시각 갱신·계정 일관).
            "received_at": _gmail_received(p),
            # ai_json 은 의도적으로 미설정 — 재동기화 시 worker 가 채운 분석 결과 보존.
        }
    elif provider == "outlook":
        frm = (p.get("from") or {}).get("emailAddress", {})
        body_obj = p.get("body") or {}
        is_html = (body_obj.get("contentType") or "").lower() == "html"
        content = body_obj.get("content") or ""
        body_html = _sanitize_html(content) if is_html else ""
        if is_html:
            body_lines = _html_to_lines(content)
        else:  # 평문: HTML 소스면 태그 제거, 아니면 주석/잔여 태그/CSS 잔재 정리.
            if _looks_html(content):
                body_lines = _html_to_lines(content)
            else:
                body_lines = _clean_plain_lines(content)
        if not body_lines and p.get("bodyPreview"):
            body_lines = [p["bodyPreview"]]
        to_list = p.get("toRecipients") or []
        to = ", ".join(
            (r.get("emailAddress", {}) or {}).get("address", "") for r in to_list
        ) or "나"
        cc = ", ".join(
            (r.get("emailAddress", {}) or {}).get("address", "")
            for r in (p.get("ccRecipients") or [])
        )
        # 인라인 이미지(cid)도 수집 → 본문 cid: 치환에 필요(프론트는 inline 칩 숨김).
        attachments = [
            {
                "id": a.get("id", ""),
                "name": a.get("name", "첨부파일"),
                "mime": a.get("contentType", "application/octet-stream"),
                "size": int(a.get("size") or 0),
                "content_id": (a.get("contentId") or "").strip("<>").strip(),
                "inline": bool(a.get("isInline")),
            }
            for a in (p.get("attachments") or [])
        ]
        fields = {
            "account": _mail_account_for(account),
            "from_key": _outlook_from(frm),
            "to": to,
            "cc": cc,
            "thread_id": p.get("conversationId", ""),
            "subject": p.get("subject") or "(제목 없음)",
            "body": body_lines,
            "body_html": body_html,
            "labels": p.get("categories") or [],
            # 폴더는 커넥터가 parentFolderId → 슬러그/표시명으로 해석해 주입(없으면 받은편지함).
            "folder": p.get("_folder", "inbox"),
            "folder_name": p.get("_folder_name", ""),
            # 집중 받은편지함 분류(focused/other) — 받은편지함=집중, 기타는 별도 폴더로 분리.
            "inference": p.get("inferenceClassification", ""),
            "read": bool(p.get("isRead", False)),
            "starred": (p.get("flag") or {}).get("flagStatus") == "flagged",
            "has_attach": bool(p.get("hasAttachments", False)),
            "attachments": attachments,
            "preview": p.get("bodyPreview", ""),
            # 표시용 time/date 는 응답 시 received_at 으로 산출(상대시각 갱신·계정 일관).
            "received_at": _parse_iso_dt(p.get("receivedDateTime", "")),
            # ai_json 미설정 — 재동기화 시 분석 결과 보존(gmail 과 동일).
        }
    else:  # mock(mail-data.js shape) / imap
        body = p.get("body", [])
        fields = {
            "account": _mail_account_for(account, p.get("account", "work")),
            "from_key": p.get("from", p.get("from_key", "")),
            "to": p.get("to", "나"),
            "subject": p.get("subject", ""),
            "body": body if isinstance(body, list) else [body],
            "labels": p.get("labels", []),
            "read": p.get("read", False),
            "starred": p.get("starred", False),
            "has_attach": p.get("hasAttach", p.get("has_attach", False)),
            "preview": p.get("preview", ""),
            "date": p.get("date", "방금"),
            "ai_json": p.get("ai", {}) or {},
        }
    return NormalizedRecord(
        entity_type="email",
        external_id=raw.external_id,
        fields=fields,
        etag=raw.etag,
        external_updated_at=raw.external_updated_at,
    )


def folder_dict(slug, name, icon, sort, kind="system", **extra) -> dict:
    """AccountFolder 항목 dict 빌더(커넥터 _sync_folders 공용)."""
    return {"slug": slug, "name": name, "kind": kind, "icon": icon, "sort_order": sort, **extra}


def replace_account_folders(
    session: Session, mail_account: str, folders: list[dict]
) -> None:
    """계정의 AccountFolder 목록을 sync 시 통째로 갱신(추가/수정/삭제)."""
    existing = {
        f.slug: f
        for f in session.exec(
            select(AccountFolder).where(AccountFolder.mail_account == mail_account)
        ).all()
    }
    seen: set[str] = set()
    for i, fd in enumerate(folders):
        slug = fd["slug"]
        seen.add(slug)
        row = existing.get(slug) or AccountFolder(
            id=f"{mail_account}:{slug}", mail_account=mail_account, slug=slug
        )
        row.name = fd["name"]
        row.kind = fd.get("kind", "system")
        row.icon = fd.get("icon", "folder")
        row.provider_id = fd.get("provider_id", "")
        row.unread = int(fd.get("unread", 0))
        row.total = int(fd.get("total", 0))
        row.sort_order = int(fd.get("sort_order", i))
        session.add(row)
    for slug, row in existing.items():
        if slug not in seen:
            session.delete(row)
    session.commit()


def upsert_email(session: Session, norm: NormalizedRecord) -> tuple[str, bool]:
    link = session.exec(
        select(ExternalLink).where(
            ExternalLink.external_id == norm.external_id,
            ExternalLink.entity_type == "email",
        )
    ).first()
    if link:  # 이미 존재 → update(멱등)
        e = session.get(Email, link.entity_id)
        if e:
            for k, v in norm.fields.items():
                if v is not None and hasattr(e, k):
                    setattr(e, k, v)
            session.add(e)
            return e.id, False
    eid = "m-" + uuid.uuid4().hex[:8]
    e = Email(id=eid, **{k: v for k, v in norm.fields.items() if v is not None})
    session.add(e)
    return eid, True
