#!/usr/bin/env python3

import asyncio
import json
import re
import sys
from pathlib import Path

import pymysql
from telethon import TelegramClient
from telethon.errors import FloodWaitError


BASE_DIR = Path(__file__).resolve().parent.parent
DB_CONFIG = BASE_DIR / "config" / "database.php"
SESSION_DIR = BASE_DIR / "engine" / "sessions"

PUBLISHER_ID = 1


def fail(message: str, code: int = 1):
    print("")
    print("❌ ERROR")
    print(message)
    raise SystemExit(code)


def read_db_config():
    text = DB_CONFIG.read_text(
        encoding="utf-8",
        errors="ignore"
    )

    patterns = {
        "host": r"\$dbHost\s*=\s*['\"]([^'\"]+)['\"]",
        "name": r"\$dbName\s*=\s*['\"]([^'\"]+)['\"]",
        "user": r"\$dbUser\s*=\s*['\"]([^'\"]+)['\"]",
        "password": r"\$dbPass\s*=\s*['\"]([^'\"]*)['\"]",
    }

    result = {}

    for key, pattern in patterns.items():
        match = re.search(pattern, text)

        if not match:
            fail(f"DB config missing: {key}")

        result[key] = match.group(1)

    return result


def get_db():
    cfg = read_db_config()

    return pymysql.connect(
        host=cfg["host"],
        user=cfg["user"],
        password=cfg["password"],
        database=cfg["name"],
        charset="utf8mb4",
        cursorclass=pymysql.cursors.DictCursor,
        autocommit=True,
    )


def get_publisher(db):
    with db.cursor() as cursor:
        cursor.execute(
            """
            SELECT *
            FROM publishers
            WHERE id = %s
              AND is_active = 1
            LIMIT 1
            """,
            (PUBLISHER_ID,)
        )

        return cursor.fetchone()


def build_client(publisher):
    api_id = int(
        str(
            publisher.get("telegram_api_id")
            or ""
        ).strip()
    )

    api_hash = str(
        publisher.get("telegram_api_hash")
        or ""
    ).strip()

    session_name = str(
        publisher.get("session_name")
        or f"publisher_{PUBLISHER_ID}"
    ).strip()

    session_name = re.sub(
        r"[^a-zA-Z0-9_-]",
        "_",
        session_name
    )

    return TelegramClient(
        str(SESSION_DIR / session_name),
        api_id,
        api_hash
    )


def get_post(db, post_id):
    with db.cursor() as cursor:
        cursor.execute(
            """
            SELECT
                p.*,
                s.username AS source_username,
                s.telegram_chat_id AS source_chat_id,
                s.name AS source_name,
                cgp.group_id,
                cgp.pool_status,
                cgp.rank_score
            FROM posts p

            LEFT JOIN sources s
                ON s.id = p.source_id

            LEFT JOIN content_group_posts cgp
                ON cgp.post_id = p.id

            WHERE p.id = %s
            ORDER BY cgp.id ASC
            LIMIT 1
            """,
            (post_id,)
        )

        return cursor.fetchone()


def get_album_posts(db, post):
    grouped_id = post.get("telegram_grouped_id")

    if not grouped_id:
        return []

    source_id = int(post.get("source_id") or 0)

    if source_id <= 0:
        return []

    with db.cursor() as cursor:
        cursor.execute(
            """
            SELECT
                id,
                source_message_id,
                telegram_grouped_id,
                text_content,
                media_type
            FROM posts
            WHERE source_id = %s
              AND telegram_grouped_id = %s
            ORDER BY source_message_id ASC
            """,
            (
                source_id,
                int(grouped_id)
            )
        )

        return cursor.fetchall()


def get_channel(db, channel_id):
    with db.cursor() as cursor:
        cursor.execute(
            """
            SELECT *
            FROM channels
            WHERE id = %s
              AND is_active = 1
            LIMIT 1
            """,
            (channel_id,)
        )

        return cursor.fetchone()


def resolve_target(channel):
    chat_id = str(
        channel.get("telegram_chat_id")
        or ""
    ).strip()

    username = str(
        channel.get("username")
        or ""
    ).strip()

    if chat_id:
        try:
            return int(chat_id)
        except ValueError:
            return chat_id

    if username:
        return username.lstrip("@")

    fail("Destination channel has no Telegram target.")


def parse_media_reference(post):
    raw = str(
        post.get("media_file_id")
        or ""
    ).strip()

    if not raw:
        return None

    try:
        data = json.loads(raw)
    except Exception:
        return None

    if data.get("provider") != "telegram":
        return None

    source_id = int(
        data.get("source_id")
        or post.get("source_id")
        or 0
    )

    message_id = int(
        data.get("message_id")
        or post.get("source_message_id")
        or 0
    )

    if source_id <= 0 or message_id <= 0:
        return None

    return {
        "source_id": source_id,
        "message_id": message_id,
    }


async def resolve_source_entity(client, post):
    """
    Resolve source without unnecessary Telegram API calls.

    Priority:
    1) telegram_chat_id from DB -> local Telethon session cache
    2) username only when no chat_id exists

    IMPORTANT:
    If chat_id exists but is missing from the local session cache,
    fail safely instead of triggering ResolveUsernameRequest.
    """

    chat_id = str(
        post.get("source_chat_id")
        or ""
    ).strip()

    if chat_id:
        try:
            numeric_chat_id = int(chat_id)

            try:
                entity = client.session.get_input_entity(
                    numeric_chat_id
                )
            except Exception as exc:
                fail(
                    "Source entity is not available in local "
                    f"Telegram session cache. Chat ID: {chat_id}. "
                    "Refusing username resolve to avoid FloodWait. "
                    f"Cache error: {exc}"
                )

            return entity

        except ValueError:
            fail(
                f"Invalid source telegram_chat_id: {chat_id}"
            )

    username = str(
        post.get("source_username")
        or ""
    ).strip().lstrip("@")

    if username:
        return await client.get_entity(username)

    fail("Source has no Telegram username/chat id.")


async def load_original_message(client, post):
    ref = parse_media_reference(post)

    if not ref:
        return None

    source_entity = await resolve_source_entity(
        client,
        post
    )

    message = await client.get_messages(
        source_entity,
        ids=ref["message_id"]
    )

    return message



def build_destination_caption(post, channel):
    """
    Build final destination caption.

    Rules:
    - Keep original content.
    - Remove attribution belonging to the SOURCE channel only.
    - Keep unrelated @mentions untouched.
    - Always append destination channel @username.
    - Media-only posts still receive destination signature.
    - Respect Telegram media caption limit.
    """

    caption = str(
        post.get("text_content")
        or ""
    ).strip()

    # -----------------------------------------------------
    # Remove source-channel attribution
    # -----------------------------------------------------

    source_username = str(
        post.get("source_username")
        or ""
    ).strip().lstrip("@")

    if source_username:

        escaped_source = re.escape(
            source_username
        )

        # Remove @source_username
        caption = re.sub(
            rf'(?<![\w@])@{escaped_source}\b',
            '',
            caption,
            flags=re.IGNORECASE
        )

        # Remove source Telegram links
        caption = re.sub(
            rf'https?://(?:www\.)?(?:t\.me|telegram\.me)/{escaped_source}(?:/[^\s]*)?',
            '',
            caption,
            flags=re.IGNORECASE
        )

        # Remove bare t.me/source links too
        caption = re.sub(
            rf'(?<![\w/])(?:t\.me|telegram\.me)/{escaped_source}(?:/[^\s]*)?',
            '',
            caption,
            flags=re.IGNORECASE
        )

    # -----------------------------------------------------
    # GLOBAL destination cleanup
    # -----------------------------------------------------
    #
    # Every destination channel must publish clean content:
    #
    # - Remove ALL @usernames inherited from the source.
    # - Remove ALL Telegram source links.
    # - The destination channel username is appended later
    #   by the normal destination-attribution logic.
    #
    # Example:
    #
    #   Original:
    #       Some content
    #       @source_one
    #       @source_two
    #       t.me/source_three
    #
    #   Destination @mychannel:
    #       Some content
    #
    #       @mychannel
    #

    # Remove every Telegram @username.
    caption = re.sub(
        r'(?<![\w@])@[A-Za-z0-9_]{4,}',
        '',
        caption
    )

    # Remove full Telegram links.
    caption = re.sub(
        r'https?://(?:www\.)?(?:t\.me|telegram\.me)/[^\s]+',
        '',
        caption,
        flags=re.IGNORECASE
    )

    # Remove bare Telegram links.
    caption = re.sub(
        r'(?<![\w/])(?:t\.me|telegram\.me)/[^\s]+',
        '',
        caption,
        flags=re.IGNORECASE
    )


    # Clean whitespace left by removed attribution.
    caption = re.sub(
        r'[ \t]+\n',
        '\n',
        caption
    )

    caption = re.sub(
        r'\n{3,}',
        '\n\n',
        caption
    ).strip()

    # -----------------------------------------------------
    # Destination attribution
    # -----------------------------------------------------

    destination_username = str(
        channel.get("username")
        or ""
    ).strip().lstrip("@")

    signature = (
        f"@{destination_username}"
        if destination_username
        else ""
    )

    if signature:
        if signature.lower() not in caption.lower():

            if caption:
                caption = (
                    caption
                    + "\n\n"
                    + signature
                )
            else:
                caption = signature

    # -----------------------------------------------------
    # Telegram media-caption limit
    # -----------------------------------------------------

    if post.get("media_type") and len(caption) > 1024:

        if signature:

            keep = (
                1024
                - len(signature)
                - 2
            )

            caption = (
                caption[
                    :max(0, keep)
                ].rstrip()
                + "\n\n"
                + signature
            )

        else:
            caption = caption[:1024]

    return caption


async def preview(post_id: int):
    db = get_db()
    client = None

    try:
        post = get_post(db, post_id)

        if not post:
            fail(f"Post #{post_id} not found.")

        publisher = get_publisher(db)

        if not publisher:
            fail("Active Publisher not found.")

        client = build_client(publisher)
        await client.connect()

        if not await client.is_user_authorized():
            fail("Telegram session is not authorized.")

        original = None
        album_posts = []

        if post.get("telegram_grouped_id"):
            album_posts = get_album_posts(
                db,
                post
            )

        if post.get("media_type"):
            original = await load_original_message(
                client,
                post
            )

        print("")
        print("CONTENT POOL PUBLISHER — PREVIEW")
        print("==============================================")
        print(f"Post ID:        {post['id']}")
        print(f"Source:         @{post.get('source_username') or '-'}")
        print(f"Source Msg ID:  {post.get('source_message_id')}")
        print(f"Content Type:   {post.get('content_type')}")
        print(f"Media Type:     {post.get('media_type') or 'NONE'}")
        print(f"Grouped ID:     {post.get('telegram_grouped_id') or '-'}")
        print(
            f"Album DB Items: "
            f"{len(album_posts) if album_posts else 0}"
        )
        print(f"Pool Status:    {post.get('pool_status') or '-'}")
        print(f"Rank Score:     {post.get('rank_score') or 0}")
        print(
            "Original Media: "
            + (
                "FOUND"
                if original is not None
                and getattr(original, "media", None)
                else (
                    "N/A"
                    if not post.get("media_type")
                    else "NOT FOUND"
                )
            )
        )
        print("----------------------------------------------")

        caption = str(
            post.get("text_content")
            or ""
        ).strip()

        # -------------------------------------------------
        # Destination channel signature
        # -------------------------------------------------

        destination_username = str(
            channel.get("username")
            or ""
        ).strip().lstrip("@")

        if destination_username:
            signature = f"@{destination_username}"

            # Do not accidentally add it twice.
            if signature.lower() not in caption.lower():
                if caption:
                    caption = (
                        caption
                        + "\n\n"
                        + signature
                    )
                else:
                    caption = signature

        # Telegram media captions are limited.
        if post.get("media_type") and len(caption) > 1024:
            signature = (
                f"@{destination_username}"
                if destination_username
                else ""
            )

            if signature:
                keep = 1024 - len(signature) - 2

                caption = (
                    caption[:max(0, keep)].rstrip()
                    + "\n\n"
                    + signature
                )
            else:
                caption = caption[:1024]

        if caption:
            print(caption)
        else:
            print("[MEDIA ONLY]")

        print("==============================================")
        print("PREVIEW ONLY — NOTHING SENT ✅")

    finally:
        if client is not None:
            try:
                await client.disconnect()
            except Exception:
                pass

        db.close()


async def test_publish(
    post_id: int,
    channel_id: int
):
    db = get_db()
    client = None

    try:
        post = get_post(db, post_id)

        if not post:
            fail(f"Post #{post_id} not found.")

        channel = get_channel(
            db,
            channel_id
        )

        if not channel:
            fail(
                f"Active Channel #{channel_id} not found."
            )

        publisher = get_publisher(db)

        if not publisher:
            fail("Active Publisher not found.")

        client = build_client(publisher)
        await client.connect()

        if not await client.is_user_authorized():
            fail("Telegram session is not authorized.")

        target = resolve_target(channel)

        caption = build_destination_caption(
            post,
            channel
        )

        try:
            if (
                post.get("media_type")
                and post.get("telegram_grouped_id")
            ):

                album_posts = get_album_posts(
                    db,
                    post
                )

                if not album_posts:
                    fail(
                        "Album grouped_id exists but "
                        "no album rows were found."
                    )

                source_entity = await resolve_source_entity(
                    client,
                    post
                )

                message_ids = [
                    int(row["source_message_id"])
                    for row in album_posts
                    if row.get("source_message_id")
                ]

                originals = await client.get_messages(
                    source_entity,
                    ids=message_ids
                )

                if not isinstance(
                    originals,
                    (list, tuple)
                ):
                    originals = [originals]

                media_files = []

                for original in originals:
                    if (
                        original is not None
                        and getattr(
                            original,
                            "media",
                            None
                        ) is not None
                    ):
                        media_files.append(
                            original.media
                        )

                if not media_files:
                    fail(
                        "No Telegram media found "
                        "for this album."
                    )

                # Telegram albums support up to 10 media.
                media_files = media_files[:10]

                sent = await client.send_file(
                    target,
                    file=media_files,
                    caption=caption or None
                )

            elif post.get("media_type"):

                original = await load_original_message(
                    client,
                    post
                )

                if (
                    original is None
                    or getattr(
                        original,
                        "media",
                        None
                    ) is None
                ):
                    fail(
                        "Original Telegram media not found."
                    )

                sent = await client.send_file(
                    target,
                    file=original.media,
                    caption=caption or None
                )

            else:

                if not caption:
                    fail(
                        "Post has neither text nor media."
                    )

                sent = await client.send_message(
                    target,
                    caption
                )

        except FloodWaitError as exc:
            fail(
                f"Telegram FloodWait: "
                f"{int(exc.seconds)} seconds"
            )

        print("")
        print("CONTENT TEST PUBLISH SUCCESS ✅")
        print("==============================================")
        print(f"Post ID:        {post_id}")
        print(f"Channel ID:     {channel_id}")
        print(f"Media Type:     {post.get('media_type') or 'TEXT'}")
        if isinstance(sent, (list, tuple)):
            sent_ids = [
                str(item.id)
                for item in sent
                if item is not None
            ]

            print(
                "Message IDs:    "
                + ",".join(sent_ids)
            )
        else:
            print(f"Message ID:     {sent.id}")
        print("==============================================")

    finally:
        if client is not None:
            try:
                await client.disconnect()
            except Exception:
                pass

        db.close()


async def main():
    if len(sys.argv) < 3:
        print(
            "Usage:\n"
            "  python3 engine/content_pool_publisher.py preview <post_id>\n"
            "  python3 engine/content_pool_publisher.py test <post_id> <channel_id>"
        )
        raise SystemExit(1)

    command = str(
        sys.argv[1]
    ).strip().lower()

    post_id = int(sys.argv[2])

    if command == "preview":
        await preview(post_id)
        return

    if command == "test":
        if len(sys.argv) < 4:
            fail(
                "test requires destination channel_id"
            )

        channel_id = int(
            sys.argv[3]
        )

        await test_publish(
            post_id,
            channel_id
        )
        return

    fail(
        f"Unknown command: {command}"
    )


if __name__ == "__main__":
    asyncio.run(main())
