from __future__ import annotations

import hashlib
import re
import sqlite3
import time
from pathlib import Path
from typing import Any, Optional

from .chat_helpers import (
    _build_latest_message_preview,
    _decode_message_content,
    _decode_sqlite_text,
    _infer_last_message_brief,
    _is_mostly_printable_text,
    _iter_message_db_paths,
    _quote_ident,
    _should_keep_session,
)
from .logging_config import get_logger
from .sqlite_diagnostics import collect_sqlite_diagnostics, format_sqlite_diagnostics

logger = get_logger(__name__)

_TABLE_NAME = "session_last_message"
_TABLE_NAME_RE = re.compile(r"^(msg_|chat_)([0-9a-f]{32})", re.IGNORECASE)
_PREVIEW_MAX_LEN = 400


def _session_db_path(account_dir: Path) -> Path:
    return Path(account_dir) / "session.db"


def _row_get(row: sqlite3.Row, key: str) -> Any:
    try:
        return row[key]
    except Exception:
        return None


def _normalize_preview(text: str) -> str:
    s = str(text or "").strip()
    if not s:
        return ""
    s = re.sub(r"\s+", " ", s).strip()
    if len(s) > _PREVIEW_MAX_LEN:
        return s[:_PREVIEW_MAX_LEN]
    return s


def _ensure_table(conn: sqlite3.Connection) -> None:
    conn.execute(
        f"""
        CREATE TABLE IF NOT EXISTS {_TABLE_NAME} (
            username TEXT PRIMARY KEY,
            sort_seq INTEGER NOT NULL DEFAULT 0,
            local_id INTEGER NOT NULL DEFAULT 0,
            create_time INTEGER NOT NULL DEFAULT 0,
            local_type INTEGER NOT NULL DEFAULT 0,
            sender_username TEXT NOT NULL DEFAULT '',
            preview TEXT NOT NULL DEFAULT '',
            db_stem TEXT NOT NULL DEFAULT '',
            table_name TEXT NOT NULL DEFAULT '',
            built_at INTEGER NOT NULL DEFAULT 0
        )
        """
    )


def get_session_last_message_status(account_dir: Path) -> dict[str, Any]:
    account_dir = Path(account_dir)
    session_db_path = _session_db_path(account_dir)
    if not session_db_path.exists():
        return {
            "status": "error",
            "account": account_dir.name,
            "message": "session.db not found.",
        }

    conn = sqlite3.connect(str(session_db_path))
    try:
        row = conn.execute(
            "SELECT name FROM sqlite_master WHERE type='table' AND lower(name)=lower(?) LIMIT 1",
            (_TABLE_NAME,),
        ).fetchone()
        exists = bool(row and row[0])
        if not exists:
            return {
                "status": "success",
                "account": account_dir.name,
                "table": {
                    "name": _TABLE_NAME,
                    "exists": False,
                    "rowCount": 0,
                    "builtAt": None,
                },
            }
        count = int(conn.execute(f"SELECT COUNT(1) FROM {_TABLE_NAME}").fetchone()[0] or 0)
        built_at = conn.execute(f"SELECT MAX(built_at) FROM {_TABLE_NAME}").fetchone()[0]
        try:
            built_at_int: Optional[int] = int(built_at) if built_at is not None else None
        except Exception:
            built_at_int = None
        return {
            "status": "success",
            "account": account_dir.name,
            "table": {
                "name": _TABLE_NAME,
                "exists": True,
                "rowCount": count,
                "builtAt": built_at_int,
            },
        }
    finally:
        conn.close()


def load_session_last_messages(account_dir: Path, usernames: list[str]) -> dict[str, str]:
    if not usernames:
        return {}

    account_dir = Path(account_dir)
    session_db_path = _session_db_path(account_dir)
    if not session_db_path.exists():
        return {}

    uniq = list(dict.fromkeys([str(u or "").strip() for u in usernames if str(u or "").strip()]))
    if not uniq:
        return {}

    out: dict[str, str] = {}
    conn = sqlite3.connect(str(session_db_path))
    conn.row_factory = sqlite3.Row
    try:
        chunk_size = 900
        for i in range(0, len(uniq), chunk_size):
            chunk = uniq[i : i + chunk_size]
            placeholders = ",".join(["?"] * len(chunk))
            rows = conn.execute(
                f"SELECT username, preview FROM {_TABLE_NAME} WHERE username IN ({placeholders})",
                chunk,
            ).fetchall()
            for r in rows:
                u = str(r["username"] or "").strip()
                if not u:
                    continue
                out[u] = str(r["preview"] or "")
        return out
    except Exception:
        return {}
    finally:
        conn.close()


def build_session_last_message_table(
    account_dir: Path,
    *,
    rebuild: bool = False,
    include_hidden: bool = True,
    include_official: bool = True,
) -> dict[str, Any]:
    """
    Build a per-account cache table `{account}/session.db::{session_last_message}`.

    The UI session list needs "last message preview" per conversation; querying message_*.db on every refresh is slow.
    This shifts that work to decrypt-time (or one-time manual rebuild).
    """

    account_dir = Path(account_dir)
    session_db_path = _session_db_path(account_dir)
    if not session_db_path.exists():
        return {
            "status": "error",
            "account": account_dir.name,
            "message": "session.db not found.",
        }

    db_paths = _iter_message_db_paths(account_dir)
    if not db_paths:
        return {
            "status": "error",
            "account": account_dir.name,
            "message": "No message databases found.",
        }

    started = time.time()
    logger.info(f"[session_last_message] build start account={account_dir.name} dbs={len(db_paths)}")

    sconn = sqlite3.connect(str(session_db_path))
    sconn.row_factory = sqlite3.Row
    try:
        try:
            srows = sconn.execute(
                """
                SELECT username, is_hidden, summary, draft, last_msg_type, last_msg_sub_type, sort_timestamp, last_timestamp
                FROM SessionTable
                ORDER BY sort_timestamp DESC
                """
            ).fetchall()
        except sqlite3.OperationalError:
            srows = sconn.execute(
                """
                SELECT username, is_hidden, summary, draft, sort_timestamp, last_timestamp
                FROM SessionTable
                ORDER BY sort_timestamp DESC
                """
            ).fetchall()
    finally:
        sconn.close()

    sessions: list[sqlite3.Row] = []
    usernames: list[str] = []
    expected_ts_by_user: dict[str, int] = {}
    for r in srows:
        u = str(_row_get(r, "username") or "").strip()
        if not u:
            continue
        if not include_hidden and int(_row_get(r, "is_hidden") or 0) == 1:
            continue
        if not _should_keep_session(u, include_official=bool(include_official)):
            continue
        sessions.append(r)
        usernames.append(u)
        ts = int(_row_get(r, "sort_timestamp") or 0)
        if ts <= 0:
            ts = int(_row_get(r, "last_timestamp") or 0)
        expected_ts_by_user[u] = int(ts or 0)

    if not usernames:
        return {
            "status": "success",
            "account": account_dir.name,
            "message": "No sessions to build.",
            "built": 0,
            "durationSec": 0.0,
        }

    md5_to_users: dict[str, list[str]] = {}
    for u in usernames:
        h = hashlib.md5(u.encode("utf-8")).hexdigest()
        md5_to_users.setdefault(h, []).append(u)

    best: dict[str, tuple[tuple[int, int, int], dict[str, Any]]] = {}

    skipped_dbs = 0
    for db_path in db_paths:
        conn: Optional[sqlite3.Connection] = None
        try:
            conn = sqlite3.connect(str(db_path))
            conn.row_factory = sqlite3.Row
            conn.text_factory = bytes
            trows = conn.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()
            md5_to_table: dict[str, str] = {}
            for tr in trows:
                if not tr or tr[0] is None:
                    continue
                name = _decode_sqlite_text(tr[0]).strip()
                if not name:
                    continue
                m = _TABLE_NAME_RE.match(name.lower())
                if not m:
                    continue
                md5_hex = str(m.group(2) or "").lower()
                if md5_hex not in md5_to_users:
                    continue
                prefix = str(m.group(1) or "").lower()
                if md5_hex not in md5_to_table or prefix == "msg_":
                    md5_to_table[md5_hex] = name

            if not md5_to_table:
                continue

            for md5_hex, table_name in md5_to_table.items():
                users = md5_to_users.get(md5_hex) or []
                if not users:
                    continue

                quoted = _quote_ident(table_name)

                row = None
                try:
                    row = conn.execute(
                        "SELECT "
                        "m.local_id, m.local_type, m.sort_seq, m.create_time, "
                        "m.message_content, m.compress_content, n.user_name AS sender_username "
                        f"FROM {quoted} m "
                        "LEFT JOIN Name2Id n ON m.real_sender_id = n.rowid "
                        "ORDER BY m.sort_seq DESC, m.local_id DESC "
                        "LIMIT 1"
                    ).fetchone()
                except Exception:
                    try:
                        row = conn.execute(
                            "SELECT "
                            "local_id, local_type, sort_seq, create_time, "
                            "message_content, compress_content, '' AS sender_username "
                            f"FROM {quoted} "
                            "ORDER BY sort_seq DESC, local_id DESC "
                            "LIMIT 1"
                        ).fetchone()
                    except Exception:
                        row = None

                if row is None:
                    continue

                try:
                    sort_seq = int(row["sort_seq"] or 0) if row["sort_seq"] is not None else 0
                except Exception:
                    sort_seq = 0
                try:
                    local_id = int(row["local_id"] or 0)
                except Exception:
                    local_id = 0
                try:
                    create_time = int(row["create_time"] or 0)
                except Exception:
                    create_time = 0

                # If session.db indicates a newer timestamp, fall back to slower but correct ordering.
                need_slow = False
                for username in users:
                    expected_ts = int(expected_ts_by_user.get(username) or 0)
                    if expected_ts > 0 and int(create_time or 0) > 0 and int(create_time or 0) < expected_ts:
                        need_slow = True
                        break

                if need_slow:
                    try:
                        row2 = conn.execute(
                            "SELECT "
                            "m.local_id, m.local_type, m.sort_seq, m.create_time, "
                            "m.message_content, m.compress_content, n.user_name AS sender_username "
                            f"FROM {quoted} m "
                            "LEFT JOIN Name2Id n ON m.real_sender_id = n.rowid "
                            "ORDER BY COALESCE(m.create_time, 0) DESC, COALESCE(m.sort_seq, 0) DESC, m.local_id DESC "
                            "LIMIT 1"
                        ).fetchone()
                    except Exception:
                        try:
                            row2 = conn.execute(
                                "SELECT "
                                "local_id, local_type, sort_seq, create_time, "
                                "message_content, compress_content, '' AS sender_username "
                                f"FROM {quoted} "
                                "ORDER BY COALESCE(create_time, 0) DESC, COALESCE(sort_seq, 0) DESC, local_id DESC "
                                "LIMIT 1"
                            ).fetchone()
                        except Exception:
                            row2 = None
                    if row2 is not None:
                        row = row2
                        try:
                            sort_seq = int(row["sort_seq"] or 0) if row["sort_seq"] is not None else 0
                        except Exception:
                            sort_seq = 0
                        try:
                            local_id = int(row["local_id"] or 0)
                        except Exception:
                            local_id = 0
                        try:
                            create_time = int(row["create_time"] or 0)
                        except Exception:
                            create_time = 0

                sort_key = (int(create_time), int(sort_seq), int(local_id))

                raw_text = _decode_message_content(row["compress_content"], row["message_content"]).strip()
                if raw_text and (not raw_text.lstrip().startswith("<")) and (not raw_text.lstrip().startswith('"<')):
                    if not _is_mostly_printable_text(raw_text):
                        raw_text = ""
                sender_username = _decode_sqlite_text(row["sender_username"]).strip()

                for username in users:
                    prev = best.get(username)
                    if prev is not None and sort_key <= prev[0]:
                        continue

                    is_group = bool(username.endswith("@chatroom"))
                    try:
                        preview = _build_latest_message_preview(
                            username=username,
                            local_type=int(row["local_type"] or 0),
                            raw_text=raw_text,
                            is_group=is_group,
                            sender_username=sender_username,
                        )
                    except Exception:
                        preview = ""
                    if preview and (not _is_mostly_printable_text(preview)):
                        try:
                            preview = _build_latest_message_preview(
                                username=username,
                                local_type=int(row["local_type"] or 0),
                                raw_text="",
                                is_group=is_group,
                                sender_username=sender_username,
                            )
                        except Exception:
                            preview = ""

                    preview = _normalize_preview(preview)
                    if not preview:
                        continue

                    best[username] = (
                        sort_key,
                        {
                            "username": username,
                            "sort_seq": int(sort_seq),
                            "local_id": int(local_id),
                            "create_time": int(create_time),
                            "local_type": int(row["local_type"] or 0),
                            "sender_username": sender_username,
                            "preview": preview,
                            "db_stem": str(db_path.stem),
                            "table_name": str(table_name),
                        },
                    )
        except sqlite3.DatabaseError as e:
            skipped_dbs += 1
            logger.warning(
                "[session_last_message] malformed message db skipped account=%s db=%s error=%s diag=%s",
                account_dir.name,
                str(db_path),
                str(e),
                format_sqlite_diagnostics(collect_sqlite_diagnostics(db_path, quick_check=True)),
            )
            continue
        finally:
            if conn is not None:
                try:
                    conn.close()
                except Exception:
                    pass

    # Fallback: always have a non-empty preview for UI.
    for r in sessions:
        u = str(_row_get(r, "username") or "").strip()
        if not u:
            continue
        if u in best:
            continue

        draft_text = _normalize_preview(_decode_sqlite_text(_row_get(r, "draft")).strip())
        if draft_text:
            preview = f"[草稿] {draft_text}" if draft_text else "[草稿]"
        else:
            summary_text = _normalize_preview(_decode_sqlite_text(_row_get(r, "summary")).strip())
            if summary_text:
                preview = summary_text
            else:
                preview = _infer_last_message_brief(_row_get(r, "last_msg_type"), _row_get(r, "last_msg_sub_type"))
        preview = _normalize_preview(preview)
        best[u] = (
            (0, 0, 0),
            {
                "username": u,
                "sort_seq": 0,
                "local_id": 0,
                "create_time": 0,
                "local_type": 0,
                "sender_username": "",
                "preview": preview,
                "db_stem": "",
                "table_name": "",
            },
        )

    built_at = int(time.time())
    conn_out = sqlite3.connect(str(session_db_path))
    try:
        _ensure_table(conn_out)
        if rebuild:
            try:
                conn_out.execute(f"DELETE FROM {_TABLE_NAME}")
            except Exception:
                pass

        rows_to_insert: list[tuple[Any, ...]] = []
        for _, rec in best.values():
            rows_to_insert.append(
                (
                    rec["username"],
                    int(rec["sort_seq"] or 0),
                    int(rec["local_id"] or 0),
                    int(rec["create_time"] or 0),
                    int(rec["local_type"] or 0),
                    str(rec["sender_username"] or ""),
                    str(rec["preview"] or ""),
                    str(rec["db_stem"] or ""),
                    str(rec["table_name"] or ""),
                    int(built_at),
                )
            )

        conn_out.executemany(
            f"INSERT OR REPLACE INTO {_TABLE_NAME}("
            "username, sort_seq, local_id, create_time, local_type, sender_username, preview, db_stem, table_name, built_at"
            ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
            rows_to_insert,
        )
        conn_out.commit()
    finally:
        conn_out.close()

    duration = max(0.0, time.time() - started)
    logger.info(
        f"[session_last_message] build done account={account_dir.name} sessions={len(best)} "
        f"durationSec={round(duration, 3)} table={_TABLE_NAME} skippedDbs={skipped_dbs}"
    )
    return {
        "status": "success",
        "account": account_dir.name,
        "built": len(best),
        "table": _TABLE_NAME,
        "durationSec": round(duration, 3),
        "skippedDbs": int(skipped_dbs),
    }
