import os
import re
import json
import sqlalchemy

from sqlalchemy.sql import select

from wurker.settings import conf
from wurker.db import make_engine, make_session
from wurker.models import WurkerBee, WurkerCommand, WurkerControl, WurkerCron


def seed_control(db_conf:object, app_id:str, **kwargs) -> bool:
    """Insert-get a wurker_control record by ID, and punnily enough, cede engine."""

    engine = make_engine(db_conf=db_conf, **kwargs)

    with engine.connect() as conn:
        stmt = select(WurkerControl).where(WurkerControl.app_id == app_id)
        control = conn.execute(stmt).first()
        if control is None:
            stmt = WurkerControl.__table__.insert().values(app_id=app_id)
            control = conn.execute(stmt)

    if control is None:
        return False

    return True


def get_control(db_conf:object, app_id:str, engine:object=None, as_dict:bool=False, **kwargs):
    """Helper to get control data."""

    engine = engine or make_engine(db_conf=db_conf, **kwargs)

    with engine.connect() as conn:
        stmt = select(WurkerControl).where(WurkerControl.app_id == app_id)
        control = conn.execute(stmt).first()

    if control is None:
        return None

    if as_dict:
        return {
            "app_id": control.app_id,
            "seed": control.seed,
            "reload": control.reload,
            "pause": control.pause,
        }

    return control


def set_control(db_conf:object, app_id:str, engine:object=None, key:str='', value=None, **kwargs):
    """Helper to set control data."""

    engine = engine or make_engine(db_conf=db_conf, **kwargs)

    kwargs = {key:value}
    with engine.connect() as conn:
        stmt = WurkerControl.__table__.update().where(
            WurkerControl.app_id == app_id
        ).values(**kwargs)

        conn.execute(stmt)


def seed_data(db_conf:object, app_id:str, data_path:str, app_root:str, **kwargs):
    """Inserts data from a JSON seed file."""

    session = make_session(db_conf, **kwargs)
    with session() as db:
        control = get_control(db_conf, app_id, engine=db.get_bind())

        pattern = r"(?P<seed>\d{3})_seed.json"
        match = re.match(pattern, os.path.basename(data_path))
        if not match:
            return None, f"Unrecognized seed data file: '{data_path}'", None

        seed = int(match["seed"])
        if control.seed >= seed and seed > 0:
            return None, None, None

        # Otherwise it's JSON, and we add with ORM
        with open(data_path) as fh:
            seed_data = json.load(fh)

        command_data = seed_data.get("wurker_command", [])
        bee_data = seed_data.get("wurker_bee", [])
        cron_data = seed_data.get("wurker_cron", [])
        bee_cron_data = seed_data.get("wurker_bee_cron", {})

        bee_slugs = map(lambda b: b['slug'], bee_data)
        bee_data.extend([
            {'slug': i, 'bee_cron': True} if i is not None else {'slug': i, 'bee_cron': False}
            for i in bee_cron_data.keys() if i not in bee_slugs
        ])

        # Track warnings
        warnings = []

        # We will re-use existing commands by slug
        existing_commands = db.query(WurkerCommand).filter(WurkerCommand.app_id == app_id).all()
        commands = {c.slug:c for c in existing_commands}
        cmd_fields = [c.key for c in WurkerCommand.__table__.columns if c.key not in ["id", "app_id"]]

        for cmd_cfg in command_data:

            # Validate data
            invalid = [k for k in cmd_cfg if k not in cmd_fields]
            if invalid:
                return seed, f"Invalid command field(s): '{invalid}'", None

            # If not is_module, we may resolve the abspath
            if not cmd_cfg["is_module"] and cmd_cfg["command"].startswith("./scripts/"):
                cmd_cfg["command"] = os.path.join(conf.etc_root, cmd_cfg["command"].replace("./scripts/", "scripts/"))

            # Skip if already configured (for now)
            if command := commands.get(cmd_cfg['slug'], None):
                warnings.append((seed, f"Already set command: '{cmd_cfg['slug']}'. Updating ..."))
            else:
                missing = [k for k in cmd_fields if k not in cmd_cfg]
                if missing:
                    return seed, f"Missing required command field(s): '{missing}'", None

                cmd_cfg["app_id"] = app_id

            try:
                if command:
                    for k, v in cmd_cfg.items():
                        if k in cmd_cfg and k != 'slug':
                            setattr(command, k, v)
                else:
                    command = WurkerCommand(**cmd_cfg)
            except Exception as ex:
                db.rollback()
                return seed, f"Exception: {ex}", None

            db.add(command)
            commands[command.slug] = command

        # We will re-use existing crons by slug
        existing_crons = db.query(WurkerCron).filter(WurkerCron.app_id == app_id).all()
        crons = {c.slug:c for c in existing_crons}
        cron_fields = [c.key for c in WurkerCron.__table__.columns if c.key not in ["id", "app_id"]]

        for cron_cfg in cron_data:

            # Validate data
            invalid = [k for k in cron_cfg if k not in cron_fields]
            if invalid:
                return seed, f"Invalid cron field(s): '{invalid}'", None

            # Update an existing cron?
            if cron := crons.get(cron_cfg['slug']):
                # Delete if the 'delete' found in seed
                if cron_cfg.pop('delete', None):
                    warnings.append((seed, f"Deleting cron: '{cron_cfg['slug']}' ..."))
                    db.delete(cron)
                    continue

                # Update if already configured
                warnings.append((seed, f"Updating existing cron: '{cron_cfg['slug']}' ..."))

            else:
                missing = [k for k in cron_fields if k not in cron_cfg]
                if missing:
                    return seed, f"Missing required cron field(s): '{missing}'", None

            cron_cfg["app_id"] = app_id
            try:
                if cron:
                    for k, v in cron_cfg.items():
                        if k in cron_fields and k != 'slug':
                            setattr(cron, k, v)
                else:
                    cron = WurkerCron(**cron_cfg)

            except Exception as ex:
                db.rollback()
                return seed, f"Exception: {ex}", None

            db.add(cron)
            if cron.slug not in crons:
                crons[cron.slug] = cron

        # Now we can relate...
        existing_bees = db.query(WurkerBee).filter(WurkerBee.app_id == app_id).all()
        bees = {b.slug:b for b in existing_bees}
        bee_fields = [
            c.key for c in WurkerBee.__table__.columns
            if c.key not in ["id", "app_id", "command_id", "quiet_restart"]
        ]
        bee_fields.append("command_slug")

        for bee_cfg in bee_data:

            is_bee_cron = bee_cfg.pop('bee_cron', None)

            # Validate data
            invalid = [k for k in bee_cfg if k not in bee_fields + ["quiet_restart", "delete"]]
            if invalid and not is_bee_cron:
                return seed, f"Invalid bee field(s): '{invalid}'", None

            command_slug = bee_cfg.get("command_slug")

            if bee := bees.get(bee_cfg['slug']):
                # Delete if the 'delete' found in seed
                if bee_cfg.pop('delete', None):
                    warnings.append((seed, f"Deleting bee: '{bee_cfg['slug']}' ..."))
                    db.delete(bee)
                    continue

                # Update if already configured
                warnings.append((seed, f"Updating existing bee: '{bee_cfg['slug']}' ..."))

            else:
                if is_bee_cron:
                    continue

                if bee_cfg['slug'] in bees:
                    return seed, f"Unknown bee slug: '{bee_cfg['slug']}'", None

                missing = [k for k in bee_fields if k not in bee_cfg]
                if missing:
                    return seed, f"Missing required bee field(s): '{missing}'", None

                if command_slug not in commands:
                    db.rollback()
                    return seed, f"Unknown command slug: '{command_slug}'", None

            if command_slug:
                del bee_cfg["command_slug"]
                bee_cfg["app_id"] = app_id

            try:
                if bee:
                    for k, v in bee_cfg.items():
                        if k in bee_fields and k != 'slug':
                            setattr(bee, k, v)
                else:
                    bee = WurkerBee(**bee_cfg)

                if bee_command := (command_slug and commands[command_slug]):
                    bee.command = bee_command

            except Exception as ex:
                db.rollback()
                return seed, f"Exception: {ex}", None

            if bee.slug in bee_cron_data:

                cron_slug = bee_cron_data[bee.slug]
                if cron_slug is None:
                    bee.crons = []
                elif cron_slug not in crons:
                    db.rollback()
                    return seed, f"Unknown cron slug: '{cron_slug}'", None
                else:
                    bee.crons = [crons[cron_slug]]

            db.add(bee)

        db.commit()

        set_control(db_conf, app_id, engine=db.get_bind(), key="reload", value=True)
        if seed > 0:
            set_control(db_conf, app_id, engine=db.get_bind(), key="seed", value=seed)

    return seed, None, warnings
