import configparser
import gzip
import json
import logging
import os
import sys
import time
from datetime import datetime

import pymysql
import pymysql.cursors
import requests

CONFIG_FILE = os.path.join(os.path.dirname(__file__), "source.conf")
STATE_FILE = os.path.join(os.path.dirname(__file__), "source.state")


def setup_logging(config):
    level = getattr(logging, config.get("logging", "level", fallback="INFO").upper())
    log_file = config.get("logging", "file", fallback=None)
    handlers = [logging.StreamHandler()]
    if log_file:
        handlers.append(logging.FileHandler(
            os.path.join(os.path.dirname(__file__), log_file)
        ))
    logging.basicConfig(
        level=level,
        format="%(asctime)s %(levelname)s %(message)s",
        handlers=handlers,
    )


def load_config():
    config = configparser.ConfigParser()
    if not os.path.exists(CONFIG_FILE):
        print(f"Konfigurationsdatei nicht gefunden: {CONFIG_FILE}", file=sys.stderr)
        sys.exit(1)
    config.read(CONFIG_FILE)
    return config


def connect_db(config):
    return pymysql.connect(
        host=config.get("database", "host"),
        port=config.getint("database", "port", fallback=3306),
        user=config.get("database", "user"),
        password=config.get("database", "password"),
        database=config.get("database", "database"),
        charset="utf8",
        cursorclass=pymysql.cursors.DictCursor,
        connect_timeout=10,
    )


def load_last_sync():
    if not os.path.exists(STATE_FILE):
        return None
    with open(STATE_FILE) as f:
        data = json.load(f)
    return data.get("last_sync")


def save_last_sync(timestamp):
    with open(STATE_FILE, "w") as f:
        json.dump({"last_sync": timestamp}, f)


def serialize_row(row):
    result = {}
    for key, value in row.items():
        if isinstance(value, datetime):
            result[key] = value.isoformat()
        else:
            result[key] = value
    return result


def fetch_changed_scheiben(cursor, last_sync, limit):
    if last_sync is None:
        cursor.execute("SELECT * FROM Scheiben ORDER BY Zeitstempel ASC")
    else:
        cursor.execute(
            "SELECT * FROM Scheiben WHERE Zeitstempel > %s "
            "ORDER BY Zeitstempel ASC LIMIT %s",
            (last_sync, limit),
        )
    return cursor.fetchall()


def fetch_serien(cursor, scheiben_id):
    cursor.execute(
        "SELECT * FROM Serien WHERE ScheibenID = %s ORDER BY Stellung, Serie",
        (scheiben_id,),
    )
    return cursor.fetchall()


def fetch_treffer(cursor, scheiben_id):
    cursor.execute(
        "SELECT * FROM Treffer WHERE ScheibenID = %s ORDER BY Stellung, Treffer",
        (scheiben_id,),
    )
    return cursor.fetchall()


def _compat_scheibe(row):
    """Ergänzt Felder, die in älteren SSMDB2-Versionen (< 5.4) fehlen."""
    if "StandNrText" not in row:
        row = dict(row)
        row["StandNrText"] = str(row.get("StandNr", ""))
    return row


def _compat_treffer(treffer_rows, serien_rows):
    """Ergänzt das Feld 'Serie' in Treffer-Zeilen für ältere SSMDB2-Versionen.

    Ältere Schemata (< 5.4) haben kein Serie-Feld in der Treffer-Tabelle.
    Die Zuordnung wird aus der gleichmäßigen Verteilung der Treffer auf die
    vorhandenen Serien je Stellung berechnet.
    """
    if not treffer_rows or "Serie" in treffer_rows[0]:
        return treffer_rows

    from collections import defaultdict
    serien_count = defaultdict(int)
    for s in serien_rows:
        serien_count[s["Stellung"]] += 1

    treffer_by_stellung = defaultdict(list)
    for t in treffer_rows:
        treffer_by_stellung[t["Stellung"]].append(t)

    result = []
    for stellung, t_list in treffer_by_stellung.items():
        n_serien = serien_count.get(stellung, 1) or 1
        per_serie = max(len(t_list) // n_serien, 1)
        for idx, t in enumerate(t_list):
            t = dict(t)
            t["Serie"] = idx // per_serie + 1
            result.append(t)

    result.sort(key=lambda t: (t["Stellung"], t["Treffer"]))
    return result


def query_target_status(config):
    """Returns the number of Scheiben on the target server, or None on error."""
    url = config.get("sync", "target_url") + "/status"
    own_token = config.get("sync", "own_token")
    target_token = config.get("sync", "target_token")
    timeout = config.getint("sync", "request_timeout", fallback=30)
    headers = {"Authorization": f"Bearer {own_token}"}
    try:
        response = requests.get(url, headers=headers, timeout=timeout)
        response.raise_for_status()
        data = response.json()
        if data.get("token") != target_token:
            logging.warning("Target-Token stimmt nicht überein bei Status-Abfrage")
            return None
        return data.get("scheiben_count", 0)
    except requests.RequestException as e:
        logging.error(f"Fehler bei Status-Abfrage: {e}")
        return None


def send_batch(config, batch, full_sync):
    url = config.get("sync", "target_url")
    own_token = config.get("sync", "own_token")
    target_token = config.get("sync", "target_token")
    timeout = config.getint("sync", "request_timeout", fallback=30)

    json_bytes = json.dumps(
        {"scheiben": batch, "full_sync": full_sync}, ensure_ascii=False
    ).encode("utf-8")
    compressed = gzip.compress(json_bytes)
    logging.debug(
        f"Payload: {len(json_bytes) / 1024:.1f} KB → {len(compressed) / 1024:.1f} KB "
        f"({len(compressed) / len(json_bytes) * 100:.0f}%)"
    )

    headers = {
        "Authorization": f"Bearer {own_token}",
        "Content-Type": "application/json",
        "Content-Encoding": "gzip",
    }
    try:
        response = requests.post(url, data=compressed, headers=headers, timeout=timeout)
        if not response.ok:
            logging.error(
                f"Fehler beim Senden: HTTP {response.status_code} — {response.text[:500]}"
            )
            return False
        data = response.json()
        if data.get("token") != target_token:
            logging.warning("Target-Token stimmt nicht überein — Verbindung nicht vertrauenswürdig")
            return False
        return True
    except requests.RequestException as e:
        logging.error(f"Fehler beim Senden: {e}")
        return False


def run_sync_cycle(config, conn):
    stands = config.getint("sync", "stands", fallback=10)
    batch_size = config.getint("sync", "batch_size", fallback=100)
    limit = stands * 2

    last_sync = load_last_sync()

    full_sync = last_sync is None
    if not full_sync:
        target_count = query_target_status(config)
        if target_count is not None and target_count == 0:
            logging.info("Target hat keine Scheiben — initiale Vollsynchronisierung")
            full_sync = True

    with conn.cursor() as cursor:
        scheiben = fetch_changed_scheiben(
            cursor, None if full_sync else last_sync, limit
        )

    if not scheiben:
        logging.debug("Keine Änderungen gefunden")
        return

    logging.info(f"{'Vollsync' if full_sync else 'Inkrementell'}: {len(scheiben)} Scheiben gefunden")

    payload = []
    newest_ts = None

    with conn.cursor() as cursor:
        for scheibe in scheiben:
            ts = scheibe["Zeitstempel"]
            if newest_ts is None or ts > newest_ts:
                newest_ts = ts
            serien = fetch_serien(cursor, scheibe["ScheibenID"])
            treffer = fetch_treffer(cursor, scheibe["ScheibenID"])
            treffer = _compat_treffer(treffer, serien)
            payload.append({
                "scheibe": serialize_row(_compat_scheibe(scheibe)),
                "serien": [serialize_row(s) for s in serien],
                "treffer": [serialize_row(t) for t in treffer],
            })

    batches = [payload[i:i + batch_size] for i in range(0, len(payload), batch_size)]
    total = len(batches)
    logging.info(
        f"Sende {len(payload)} Scheiben in {total} Batch{'es' if total != 1 else ''} "
        f"(max. {batch_size} pro Batch)"
    )

    for i, batch in enumerate(batches, 1):
        is_first = (i == 1)
        logging.info(f"Batch {i}/{total}: {len(batch)} Scheiben")
        if not send_batch(config, batch, full_sync and is_first):
            logging.error(f"Batch {i}/{total} fehlgeschlagen — Sync abgebrochen")
            return

    if newest_ts:
        ts_str = newest_ts.isoformat() if isinstance(newest_ts, datetime) else newest_ts
        save_last_sync(ts_str)
    logging.info(f"Sync abgeschlossen: {len(payload)} Scheiben in {total} Batches übertragen")


def main():
    config = load_config()
    setup_logging(config)
    logging.info("meyton-sync Source gestartet")

    interval = config.getint("sync", "interval", fallback=60)
    logging.info(f"Sync-Intervall: {interval}s")

    while True:
        try:
            conn = connect_db(config)
            try:
                run_sync_cycle(config, conn)
            finally:
                conn.close()
        except pymysql.Error as e:
            logging.error(f"Datenbankfehler: {e}")
        except Exception as e:
            logging.exception(f"Unerwarteter Fehler: {e}")

        time.sleep(interval)


if __name__ == "__main__":
    main()
