#!/usr/bin/env python3
"""Post-import automation for the LAN Redmine development clone.

Run this after the production database and Redmine tree have been copied onto
the dev/test host. It reapplies this repository's tracked plugin forks, runs
plugin migrations, sanitizes Helpdesk mail settings, restarts Passenger, and
performs validation. The default mode is a dry run.
"""

import argparse
import datetime as dt
import json
import os
import shlex
import subprocess
import sys
import socket
from pathlib import Path
from typing import List, Optional, Tuple


DEFAULT_SSH_HOST = "reddev@192.168.50.170"
DEFAULT_SSH_KEY = Path("/tmp/reddev")
DEFAULT_REMOTE_REDMINE = "/usr/share/redmine"
DEFAULT_REMOTE_OVERRIDES = "/home/reddev/redmine-dev-overrides"
DEFAULT_FILES_ROOT = "/var/lib/redmine/default/files"
DEFAULT_MAILPIT_HOST = "192.168.1.105"
DEFAULT_SEMANTIC_LIMITS = "customer-service=5"
DEFAULT_LANSCRATCH_ROOT = Path("/opt/lanscratch/redmine-post-import")
DEFAULT_STATUS_DIR = DEFAULT_LANSCRATCH_ROOT / "status"
DEFAULT_LOCAL_REPO_ROOT = DEFAULT_LANSCRATCH_ROOT / "repo"

TRACKED_PLUGINS = (
    "redmine_event_outbox",
    "redmine_contacts",
    "redmine_contacts_helpdesk",
)


class AutomationConfig:
    def __init__(
        self,
        apply=False,
        local=False,
        repo_root=None,
        status_dir=DEFAULT_STATUS_DIR,
        ssh_host=DEFAULT_SSH_HOST,
        ssh_key=DEFAULT_SSH_KEY,
        remote_redmine=DEFAULT_REMOTE_REDMINE,
        remote_overrides=DEFAULT_REMOTE_OVERRIDES,
        files_root=DEFAULT_FILES_ROOT,
        mailpit_host=DEFAULT_MAILPIT_HOST,
        semantic_limits=DEFAULT_SEMANTIC_LIMITS,
        remote_sudo=True,
        skip_semantic_check=False,
        skip_helpdesk_reset=False,
    ):
        self.apply = apply
        self.local = local
        self.repo_root = repo_root if repo_root is not None else (DEFAULT_LOCAL_REPO_ROOT if local else Path("."))
        self.status_dir = status_dir
        self.ssh_host = ssh_host
        self.ssh_key = ssh_key
        self.remote_redmine = remote_redmine
        self.remote_overrides = remote_overrides
        self.files_root = files_root
        self.mailpit_host = mailpit_host
        self.semantic_limits = semantic_limits
        self.remote_sudo = remote_sudo
        self.skip_semantic_check = skip_semantic_check
        self.skip_helpdesk_reset = skip_helpdesk_reset


class Step:
    def __init__(self, name, commands):
        self.name = name
        self.commands = commands


class StepResult:
    def __init__(self, step, command, returncode):
        self.step = step
        self.command = command
        self.returncode = returncode


def main() -> int:
    args = parse_args()
    config = AutomationConfig(
        apply=args.apply,
        local=args.local,
        repo_root=args.repo_root or (DEFAULT_LOCAL_REPO_ROOT if args.local else Path(".")),
        status_dir=args.status_dir,
        ssh_host=args.ssh_host,
        ssh_key=args.ssh_key,
        remote_redmine=args.remote_redmine,
        remote_overrides=args.remote_overrides,
        files_root=args.files_root,
        mailpit_host=args.mailpit_host,
        semantic_limits=args.semantic_limits,
        remote_sudo=not args.no_remote_sudo,
        skip_semantic_check=args.skip_semantic_check,
        skip_helpdesk_reset=args.skip_helpdesk_reset,
    )
    steps = build_steps(config)
    run_id = utc_stamp()
    results = []  # type: List[StepResult]

    print(f"mode={'apply' if config.apply else 'dry-run'}")
    print(f"execution={'local' if config.local else 'remote'}")
    if config.apply:
        write_status(config, run_id, "running", results)
    for step in steps:
        print(f"\n== {step.name} ==")
        for command in step.commands:
            if config.apply:
                print(f"running: {command}")
                result = subprocess.run(command, shell=True, check=False)
                results.append(StepResult(step.name, command, result.returncode))
                write_status(config, run_id, "running", results)
                if result.returncode != 0:
                    print(f"error: command failed with exit {result.returncode}", file=sys.stderr)
                    write_status(config, run_id, "failed", results, failed_step=step.name)
                    return result.returncode
            else:
                print(f"would run: {command}")

    if not config.apply:
        print("\nDry run only. Re-run with --apply after reviewing the command list.")
    else:
        write_status(config, run_id, "success", results)
    return 0


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Run daily post-import steps for the LAN Redmine dev clone."
    )
    parser.add_argument("--apply", action="store_true", help="Run commands. Default is dry-run.")
    parser.add_argument(
        "--local",
        action="store_true",
        help="Run directly on the Redmine test host instead of orchestrating over SSH.",
    )
    parser.add_argument(
        "--repo-root",
        type=Path,
        default=Path(os.environ["POST_IMPORT_REPO_ROOT"]) if "POST_IMPORT_REPO_ROOT" in os.environ else None,
        help="Repository/payload root to copy plugins and run helper scripts from.",
    )
    parser.add_argument(
        "--status-dir",
        type=Path,
        default=Path(os.getenv("POST_IMPORT_STATUS_DIR", str(DEFAULT_STATUS_DIR))),
    )
    parser.add_argument("--ssh-host", default=os.getenv("REDMINE_SSH_HOST", DEFAULT_SSH_HOST))
    parser.add_argument(
        "--ssh-key",
        type=Path,
        default=Path(os.getenv("REDMINE_SSH_KEY", str(DEFAULT_SSH_KEY))),
    )
    parser.add_argument(
        "--remote-redmine",
        default=os.getenv("REDMINE_REMOTE_PATH", DEFAULT_REMOTE_REDMINE),
    )
    parser.add_argument(
        "--remote-overrides",
        default=os.getenv("REDMINE_REMOTE_OVERRIDES", DEFAULT_REMOTE_OVERRIDES),
        help=(
            "Remote directory containing dev-only files to overlay after the production copy. "
            "If it is absent on the remote host, the step is skipped."
        ),
    )
    parser.add_argument("--files-root", default=DEFAULT_FILES_ROOT)
    parser.add_argument("--mailpit-host", default=DEFAULT_MAILPIT_HOST)
    parser.add_argument("--semantic-limits", default=DEFAULT_SEMANTIC_LIMITS)
    parser.add_argument(
        "--no-remote-sudo",
        action="store_true",
        help="Do not use sudo for remote write/permission steps.",
    )
    parser.add_argument("--skip-semantic-check", action="store_true")
    parser.add_argument("--skip-helpdesk-reset", action="store_true")
    return parser.parse_args()


def build_steps(config):
    steps = [
        Step(
            "preflight",
            preflight_commands(config),
        ),
        Step(
            "restore dev-local overrides",
            (
                restore_overrides_command(config),
            ),
        ),
        Step(
            "reapply tracked plugins",
            (
                copy_plugins_command(config),
            ),
        ),
        Step(
            "run plugin migrations",
            (
                redmine_command(
                    config,
                    "RAILS_ENV=production bundle exec rake redmine:plugins:migrate",
                ),
            ),
        ),
        Step(
            "fix dev writable paths",
            (
                fix_permissions_command(config),
            ),
        ),
    ]

    if not config.skip_helpdesk_reset:
        steps.append(
            Step(
                "reset Helpdesk mail settings",
                (
                    helpdesk_reset_command(config),
                ),
            )
        )

    steps.extend(
        [
            Step(
                "restart Passenger",
                (
                    redmine_command(config, "touch tmp/restart.txt"),
                ),
            ),
            Step(
                "validate test instance",
                (
                    validate_command(config),
                ),
            ),
            Step(
                "check outbox worker",
                outbox_commands(config),
            ),
        ]
    )

    if not config.skip_semantic_check:
        steps.append(
            Step(
                "validate semantic index dry-run",
                (
                    semantic_check_command(config),
                ),
            )
        )

    return steps


def preflight_commands(config):
    commands = tuple(local_test(str(config.repo_root / "plugins" / plugin)) for plugin in TRACKED_PLUGINS)
    if config.local:
        return commands + (f"test -d {q(config.remote_redmine)}",)
    return commands + (ssh(config, f"test -d {q(config.remote_redmine)}"),)


def semantic_check_command(config):
    refresh = config.repo_root / "semantic_index" / "refresh.sh"
    python_bin = config.repo_root / ".venv" / "bin" / "python"
    command = "SEMANTIC_INDEX_PROJECT_LIMITS={limits} {refresh}".format(
        limits=q(config.semantic_limits),
        refresh=q(str(refresh)),
    )
    if not config.local:
        return command
    return (
        "if test -x {python}; then PYTHON={python} {command}; "
        "else echo 'semantic index runtime missing; skipping dry-run'; fi"
    ).format(
        python=q(str(python_bin)),
        command=command,
    )


def local_test(path):
    return f"test -d {q(path)}"


def restore_overrides_command(config):
    command = "if [ -d {overrides} ]; then {sudo}rsync -a {overrides}/ {redmine}/; else echo 'no dev overrides directory: {overrides}'; fi".format(
        sudo=remote_sudo_prefix(config),
        overrides=q(config.remote_overrides),
        redmine=q(config.remote_redmine),
    )
    return command if config.local else ssh(config, command)


def copy_plugins_command(config):
    sources = " ".join(q(str(config.repo_root / "plugins" / plugin)) for plugin in TRACKED_PLUGINS)
    if config.local:
        return f"rsync -a --delete {sources} {q(config.remote_redmine + '/plugins/')}"
    return (
        "rsync -a --delete "
        f"{rsync_path_option(config)}"
        f"-e {q(ssh_transport(config))} "
        f"{sources} "
        f"{q(config.ssh_host + ':' + config.remote_redmine + '/plugins/')}"
    )


def redmine_command(config, command):
    full = f"cd {q(config.remote_redmine)} && {command}"
    return full if config.local else ssh(config, full)


def fix_permissions_command(config):
    command = "{sudo}mkdir -p {files} {redmine}/tmp {redmine}/log && {sudo}chmod -R g+rwX {files} {redmine}/tmp {redmine}/log && {sudo}find {files} -type d -exec chmod g+s {{}} +".format(
        sudo=remote_sudo_prefix(config),
        files=q(config.files_root),
        redmine=q(config.remote_redmine),
    )
    return command if config.local else ssh(config, command)


def script(config, name):
    path = config.repo_root / name
    if str(config.repo_root) == ".":
        return q(f"./{name}")
    return q(str(path))


def helpdesk_reset_command(config):
    base = f"{script(config, 'reset_helpdesk_mail_settings.py')} "
    if config.local:
        return (
            base +
            f"--local --remote-redmine {q(config.remote_redmine)} "
            f"--mailpit-host {q(config.mailpit_host)}"
        )
    return (
        base +
        f"--ssh-host {q(config.ssh_host)} "
        f"--ssh-key {q(str(config.ssh_key))} "
        f"--remote-redmine {q(config.remote_redmine)} "
        f"--mailpit-host {q(config.mailpit_host)}"
    )


def validate_command(config):
    base = f"{script(config, 'validate_test_instance.py')} "
    if config.local:
        return (
            base +
            f"--local --remote-redmine {q(config.remote_redmine)} "
            f"--mailpit-host {q(config.mailpit_host)} "
            f"--files-root {q(config.files_root)}"
        )
    return (
        base +
        f"--ssh-host {q(config.ssh_host)} "
        f"--ssh-key {q(str(config.ssh_key))} "
        f"--remote-redmine {q(config.remote_redmine)} "
        f"--mailpit-host {q(config.mailpit_host)} "
        f"--files-root {q(config.files_root)}"
    )


def outbox_commands(config):
    base = script(config, "redmine_outbox_worker.py")
    local = " --local" if config.local else ""
    return (
        f"{base}{local} --status",
        f"{base}{local} --dry-run --batch-size 10",
    )


def ssh(config, remote_command):
    return (
        "ssh "
        f"-i {q(str(config.ssh_key))} "
        "-o IdentitiesOnly=yes "
        f"{q(config.ssh_host)} "
        f"{q(remote_command)}"
    )


def ssh_transport(config):
    return f"ssh -i {str(config.ssh_key)} -o IdentitiesOnly=yes"


def remote_sudo_prefix(config):
    return "sudo " if config.remote_sudo else ""


def rsync_path_option(config):
    return "--rsync-path 'sudo rsync' " if config.remote_sudo else ""


def write_status(
    config: AutomationConfig,
    run_id: str,
    status: str,
    results,
    failed_step=None,
):
    now = utc_iso()
    document = {
        "run_id": run_id,
        "started_at": run_id_to_iso(run_id),
        "updated_at": now,
        "finished_at": now if status in {"success", "failed"} else None,
        "mode": "apply" if config.apply else "dry-run",
        "execution": "local" if config.local else "remote",
        "host": socket.gethostname(),
        "remote_redmine": config.remote_redmine,
        "repo_root": str(config.repo_root),
        "status": status,
        "failed_step": failed_step,
        "steps": [
            {"step": item.step, "command": item.command, "returncode": item.returncode}
            for item in results
        ],
    }
    config.status_dir.mkdir(parents=True, exist_ok=True)
    runs_dir = config.status_dir / "runs"
    runs_dir.mkdir(parents=True, exist_ok=True)
    write_json(runs_dir / f"{run_id}.json", document)
    write_json(config.status_dir / "latest.json", document)
    if status == "success":
        write_json(config.status_dir / "latest-success.json", document)
    return document


def write_json(path, document):
    tmp = path.with_suffix(path.suffix + ".tmp")
    tmp.write_text(json.dumps(document, indent=2, sort_keys=True) + "\n", encoding="utf-8")
    tmp.replace(path)


def utc_stamp():
    return dt.datetime.now(dt.timezone.utc).strftime("%Y%m%dT%H%M%SZ")


def utc_iso():
    return dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def run_id_to_iso(run_id):
    try:
        parsed = dt.datetime.strptime(run_id, "%Y%m%dT%H%M%SZ").replace(tzinfo=dt.timezone.utc)
        return parsed.strftime("%Y-%m-%dT%H:%M:%SZ")
    except ValueError:
        return run_id


def q(value):
    return shlex.quote(value)


if __name__ == "__main__":
    raise SystemExit(main())
