"""Старт инфраструктуры: embedded PostgreSQL (pgserver) + fakeredis (без Docker)."""
from __future__ import annotations

import sys
import time
from pathlib import Path

_PKG_ROOT = Path(__file__).resolve().parents[1]
if str(_PKG_ROOT) not in sys.path:
    sys.path.insert(0, str(_PKG_ROOT))

try:
    from spamimpakter.runtime_base import data_root as _data_root
    ROOT = _data_root()
except Exception:
    ROOT = _PKG_ROOT

PGDATA = ROOT / "data" / "pgdata"
ENV_FILE = ROOT / ".runtime.env"


def _bundle_root() -> Path:
    """Каталог со встроенными ресурсами (_scripts/*) — рядом с .exe во frozen."""
    try:
        from spamimpakter.runtime_base import bundle_dir
        return bundle_dir()
    except Exception:
        return _PKG_ROOT


def _pg_table_exists(dsn: str, table: str) -> bool:
    """Есть ли таблица в PG (sentinel для определения свежей базы)."""
    try:
        import psycopg2  # type: ignore
    except ImportError:
        return False
    try:
        conn = psycopg2.connect(dsn, connect_timeout=5)
    except Exception:
        return False
    try:
        cur = conn.cursor()
        cur.execute("SELECT to_regclass(%s)", (table,))
        return (cur.fetchone() or [None])[0] is not None
    finally:
        conn.close()


def _bootstrap_fresh_schema(dsn: str) -> None:
    """Развернуть ПОЛНУЮ схему PG на чистой базе из дампа реальной рабочей базы.

    Источник правды — _scripts/pg_schema_full.sql (pg_dump --schema-only с dev-базы):
    ВСЕ таблицы, колонки, UNIQUE-индексы и ограничения 1-в-1 как на рабочей машине.
    Это убирает целый класс багов на чистых машинах («нет колонки», «no unique
    constraint matching ON CONFLICT» и т.п.), которые возникали, когда схема
    собиралась из неполного SQLite-снапшота.
    """
    import psycopg2  # type: ignore

    sql_path = _bundle_root() / "_scripts" / "pg_schema_full.sql"
    if not sql_path.exists():
        print(f"  [warn] дамп схемы отсутствует: {sql_path} — fallback на снапшот")
        _bootstrap_fresh_schema_from_snapshot(dsn)
        return

    sql_text = sql_path.read_text(encoding="utf-8")
    conn = psycopg2.connect(dsn, connect_timeout=15)
    conn.autocommit = False
    try:
        cur = conn.cursor()
        cur.execute(sql_text)  # pg_dump-скрипт без psql-метакоманд → один execute
        conn.commit()
        # сколько таблиц реально создалось
        cur.execute(
            "SELECT count(*) FROM information_schema.tables "
            "WHERE table_schema='public' AND table_type='BASE TABLE'"
        )
        n = (cur.fetchone() or [0])[0]
        print(f"  Развёрнута полная PG-схема из дампа: {n} таблиц")
    except Exception as e:
        conn.rollback()
        print(f"  [warn] применение дампа схемы не удалось: {e} — fallback на снапшот")
        _bootstrap_fresh_schema_from_snapshot(dsn)
    finally:
        conn.close()


def _bootstrap_fresh_schema_from_snapshot(dsn: str) -> None:
    """Fallback: развернуть схему из SQLite-снапшота (если нет SQL-дампа).

    На машине пользователя нет dev-SQLite для зеркалирования, поэтому полная
    структура (все таблицы + индексы, без данных) берётся из бандл-ресурса
    _scripts/pg_schema_template.db. Структуру в PG переносим теми же функциями,
    что и обычная миграция (_gen_pg_schema), чтобы схема была идентичной.
    """
    import importlib.util
    import re
    import sqlite3

    import psycopg2  # type: ignore

    template = _bundle_root() / "_scripts" / "pg_schema_template.db"
    if not template.exists():
        print(f"  [warn] schema-снапшот отсутствует: {template}")
        return

    gen_path = _bundle_root() / "_scripts" / "_gen_pg_schema.py"
    spec = importlib.util.spec_from_file_location("_gen_pg_schema", str(gen_path))
    gen = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(gen)

    sl = sqlite3.connect(str(template))
    pg = psycopg2.connect(dsn, connect_timeout=5)
    pg.autocommit = False
    cur = pg.cursor()
    try:
        tables = gen._get_tables(sl)
        created = 0
        for t in tables:
            try:
                cur.execute(gen._create_table_sql(sl, t))
                created += 1
            except Exception as e:
                pg.rollback()
                cur = pg.cursor()
                print(f"  [warn] CREATE {t}: {e}")
        pg.commit()

        idx_rows = sl.execute(
            "SELECT sql FROM sqlite_master WHERE type='index' AND sql IS NOT NULL"
        ).fetchall()
        for (idx_sql,) in idx_rows:
            m = re.match(
                r"CREATE\s+(UNIQUE\s+)?INDEX\s+(\w+)\s+ON\s+(\w+)\s*\(([^)]+)\)",
                idx_sql or "",
                re.IGNORECASE,
            )
            if not m:
                continue
            unique = "UNIQUE " if m.group(1) else ""
            cols = ", ".join(f'"{c.strip()}"' for c in m.group(4).split(","))
            try:
                cur.execute(
                    f'CREATE {unique}INDEX IF NOT EXISTS "{m.group(2)}" '
                    f'ON "{m.group(3)}" ({cols})'
                )
            except Exception:
                pg.rollback()
                cur = pg.cursor()
        pg.commit()
        print(f"  Развёрнута PG-схема из снапшота: {created}/{len(tables)} таблиц")
    finally:
        sl.close()
        pg.close()

    # PG-нативные таблицы, которых нет в SQLite-снапшоте (settings_kv и пр.).
    try:
        import psycopg2  # type: ignore

        conn = psycopg2.connect(dsn, connect_timeout=5)
        conn.autocommit = True
        try:
            cur2 = conn.cursor()
            cur2.execute(
                "CREATE TABLE IF NOT EXISTS settings_kv ("
                "key TEXT PRIMARY KEY, value TEXT NOT NULL)"
            )
        finally:
            conn.close()

        from spamimpakter.db import PgStore

        st = PgStore(dsn=dsn, minconn=1, maxconn=2, redis_url="fakeredis://")
        try:
            with st._conn() as conn2:
                with conn2.cursor() as c:
                    st._ensure_runtime_indexes(c)
        except Exception:
            pass
        finally:
            st.close()
        print("  PG-нативные таблицы (settings_kv) созданы")
    except Exception as e:
        print(f"  [warn] PG-native tables: {e}")


def _ensure_database(admin_uri: str, db_name: str = "spamimpakter") -> None:
    """Создать базу данных если не существует."""
    try:
        import psycopg2  # type: ignore
    except ImportError:
        print(f"  [!] psycopg2 не установлен — пропускаю создание БД")
        return

    # admin_uri указывает на postgres db; меняем базу для connect
    admin_uri_pg = admin_uri
    if not admin_uri_pg.endswith("/postgres"):
        # Если pgserver вернул другой путь — используем как есть (postgres)
        pass

    conn = None
    last_err = None
    for _ in range(90):  # до ~90с на cold start PG (shared_buffers=1GB стартует дольше)
        try:
            conn = psycopg2.connect(admin_uri_pg, connect_timeout=3)
            break
        except Exception as e:
            last_err = e
            time.sleep(1)
    if conn is None:
        raise RuntimeError(f"PG connect failed: {last_err}")
    conn.autocommit = True
    cur = conn.cursor()
    cur.execute("SELECT 1 FROM pg_database WHERE datname=%s", (db_name,))
    if not cur.fetchone():
        print(f"  Создаю базу {db_name}...")
        cur.execute(f"CREATE DATABASE {db_name}")
    cur.close()
    conn.close()


def _read_port_from_pid_file() -> int | None:
    """Читает актуальный порт из postmaster.pid (строка 4)."""
    pid_file = PGDATA / "postmaster.pid"
    if not pid_file.exists():
        return None
    try:
        lines = pid_file.read_text(encoding="utf-8", errors="replace").splitlines()
        # postmaster.pid: line 1=pid, line 2=pgdata, line 3=start time, line 4=port
        if len(lines) >= 4:
            return int(lines[3].strip())
    except Exception:
        pass
    return None


def _tune_postgresql_conf() -> bool:
    """Production-grade настройки PG под 500+ аккаунтов.

    Применяются ДО первого запуска embedded PG (требуют рестарт сервера).
    Если конфиг уже модифицирован — пропускаем.
    """
    cfg = PGDATA / "postgresql.conf"
    if not cfg.exists():
        return False
    text = cfg.read_text(encoding="utf-8", errors="replace")
    if "# spamimpakter-tuned" in text:
        return False  # уже настроено

    targets = {
        r"^max_connections\s*=\s*\d+": "max_connections = 600",
        r"^shared_buffers\s*=\s*\S+": "shared_buffers = 256MB",
        r"^#?work_mem\s*=\s*\S+": "work_mem = 16MB",
        r"^#?maintenance_work_mem\s*=\s*\S+": "maintenance_work_mem = 128MB",
        r"^#?effective_cache_size\s*=\s*\S+": "effective_cache_size = 1GB",
        r"^#?wal_buffers\s*=\s*\S+": "wal_buffers = 16MB",
        r"^#?random_page_cost\s*=\s*\S+": "random_page_cost = 1.1",
        r"^#?checkpoint_completion_target\s*=\s*\S+": "checkpoint_completion_target = 0.9",
        r"^#?max_worker_processes\s*=\s*\S+": "max_worker_processes = 8",
        r"^#?max_parallel_workers_per_gather\s*=\s*\S+": "max_parallel_workers_per_gather = 4",
        r"^#?max_parallel_workers\s*=\s*\S+": "max_parallel_workers = 8",
    }
    import re
    new_text = text
    for pat, replacement in targets.items():
        old = new_text
        new_text = re.sub(pat, replacement, new_text, count=1, flags=re.MULTILINE)
        if new_text == old and replacement.split()[0] not in old:
            new_text += f"\n{replacement}"
    new_text += "\n# spamimpakter-tuned\n"
    cfg.write_text(new_text, encoding="utf-8")
    print("  PostgreSQL конфиг настроен под production (max_connections=600, shared_buffers=1GB)")
    return True


def _start_embedded_pg() -> str:
    """Запускает embedded pgserver и возвращает DSN к spamimpakter БД."""
    try:
        import pgserver  # type: ignore
    except ImportError:
        print("[!] pgserver не установлен. Запусти: .venv\\Scripts\\pip install pgserver")
        return ""

    PGDATA.mkdir(parents=True, exist_ok=True)
    print(f"  Запускаю embedded PostgreSQL: {PGDATA}")

    # Применяем production-настройки до запуска
    _tune_postgresql_conf()

    base_uri: str = ""
    try:
        # cleanup_mode=None → сервер продолжает жить после выхода этого процесса
        server = pgserver.get_server(pgdata=str(PGDATA), cleanup_mode=None)
        base_uri = server.get_uri()
    except Exception as e:
        # На Windows pg_ctl -w иногда падает по таймауту из-за блокировки log-файла,
        # но postgres реально уже слушает. Берём порт из postmaster.pid.
        print(f"  [warn] pgserver: {e}")
        port = _read_port_from_pid_file()
        if port:
            print(f"  Postgres уже запущен (postmaster.pid, port={port})")
            base_uri = f"postgresql://postgres:@127.0.0.1:{port}/postgres"
        else:
            return ""

    print(f"  PostgreSQL запущен: {base_uri}")

    # Создаём нашу базу данных
    _ensure_database(base_uri, "spamimpakter")

    # DSN всегда из postmaster.pid — pgserver иногда возвращает другой порт в get_uri()
    port = _read_port_from_pid_file()
    if port:
        dsn = f"postgresql://postgres:@127.0.0.1:{port}/spamimpakter"
    else:
        from urllib.parse import urlparse, urlunparse
        parsed = urlparse(base_uri)
        dsn = urlunparse(parsed._replace(path="/spamimpakter"))

    return dsn


def _sqlite_account_count() -> int:
    import sqlite3
    db = ROOT / "data" / "spamimpakter.db"
    if not db.exists():
        return 0
    conn = sqlite3.connect(db)
    try:
        row = conn.execute("SELECT COUNT(*) FROM accounts").fetchone()
        return int(row[0]) if row else 0
    except sqlite3.Error:
        return 0
    finally:
        conn.close()


def _pg_account_count(dsn: str) -> int:
    try:
        from spamimpakter.db import PgStore
        store = PgStore(dsn=dsn, minconn=1, maxconn=2, redis_url="fakeredis://")
        try:
            store.init_db()
            with store._conn() as conn:
                with conn.cursor() as cur:
                    cur.execute("SELECT COUNT(*) FROM accounts")
                    row = cur.fetchone()
                    return int(row[0]) if row else 0
        finally:
            store.close()
    except Exception as e:
        print(f"  [warn] pg_account_count: {e}")
        return 0


def _migrate_if_needed(dsn: str) -> None:
    sqlite_n = _sqlite_account_count()
    if sqlite_n <= 0:
        return
    pg_n = _pg_account_count(dsn)
    if pg_n >= sqlite_n:
        return
    # Полная зеркальная миграция SQLite → PG (пересоздаёт схему и данные)
    try:
        print(f"  Зеркальная миграция SQLite → PG ({sqlite_n} аккаунтов)...")
        import sys as _sys
        old_argv = _sys.argv[:]
        _sys.argv = [str(ROOT / "_scripts/_gen_pg_schema.py")]
        try:
            import importlib.util as _ilu
            spec = _ilu.spec_from_file_location(
                "_gen_pg_schema",
                ROOT / "_scripts/_gen_pg_schema.py",
            )
            mod = _ilu.module_from_spec(spec)
            spec.loader.exec_module(mod)
            mod.main()
        finally:
            _sys.argv = old_argv
    except Exception as e:
        print(f"  [warn] Миграция: {e}")
        # Fallback на postgres_migration убран: модуль не включён в .exe-сборку
        # (build_nuitka.bat не упаковывает postgres_migration/), в frozen это
        # давало ModuleNotFoundError поверх первой ошибки. На чистой PG схема
        # разворачивается из снапшота через _bootstrap_fresh_schema (см. выше).


def main() -> int:
    print("[1/2] Запуск embedded PostgreSQL...")
    dsn = _start_embedded_pg()
    if not dsn:
        print("[!] Не удалось запустить embedded PostgreSQL")
        return 1

    print("[2/2] Проверка и миграция...")
    try:
        # Чистая машина: нет dev-SQLite для зеркалирования и в PG ещё нет полной
        # схемы (sentinel — broadcast_recipients). Разворачиваем её из снапшота.
        if not _pg_table_exists(dsn, "broadcast_recipients"):
            print("  Чистая PG-база — разворачиваю полную схему из снапшота...")
            _bootstrap_fresh_schema(dsn)
        _migrate_if_needed(dsn)
    except Exception as e:
        print(f"[warn] Миграция: {e}")

    # Сохраняем DSN в .runtime.env — app.py читает его при старте
    # Redis: используем fakeredis (встроенный, без сервера)
    ENV_FILE.write_text(
        f"PG_DSN={dsn}\nREDIS_URL=fakeredis://\nSPAMIMPAKTER_USE_PG=1\n",
        encoding="utf-8",
    )
    print(f"\nDSN сохранён: {dsn}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
