#!/usr/bin/env python3
"""
change_ldap_parameters.py - Configure external LDAP in /etc/mmc/plugins/base.ini.local

Usage: change_ldap_parameters.py \\
            --base-dn "dc=domain,dc=local" \\
            --fqdn-ldap "ldaps://ad-server.domain.local" \\
            --dn-bind "CN=medulla.bind,OU=Infra,DC=domain,DC=local" \\
            --pass-bind "MyPassword" \\
            --attr "sAMAccountName" \\
            --givenname "givenName" \\
            --surname "sn"

The port is automatically added: 389 for ldap://, 636 for ldaps://
"""

import argparse
import re
import shutil
import sys
from datetime import datetime
from pathlib import Path

BASE_INI_PATH = "/etc/mmc/plugins/base.ini.local"


# =============================================================================
# Argument parsing
# =============================================================================

def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Configure external LDAP in base.ini.local",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=__doc__,
    )
    parser.add_argument(
        "-b", "--base-dn",
        required=True,
        metavar="BASE_DN",
        help="Base DN (e.g. dc=domain,dc=local)",
    )
    parser.add_argument(
        "-l", "--fqdn-ldap",
        required=True,
        metavar="FQDN_LDAP",
        help="LDAP URL (e.g. ldap://server.domain.local or ldaps://server.domain.local)",
    )
    parser.add_argument(
        "-d", "--dn-bind",
        required=True,
        metavar="DN_BIND",
        help="Bind DN (e.g. CN=medulla.bind,OU=Infra,DC=domain,DC=local)",
    )
    parser.add_argument(
        "-p", "--pass-bind",
        required=True,
        metavar="PASS_BIND",
        help="Bind account password",
    )
    parser.add_argument(
        "-a", "--attr",
        required=True,
        metavar="ATTR",
        help="LDAP attribute (e.g. sAMAccountName)",
    )
    parser.add_argument(
        "-gn", "--givenname",
        required=False,
        default="givenName",
        metavar="GN",
        help="LDAP attribute for given name (e.g. givenName)",
    )
    parser.add_argument(
        "-sn", "--surname",
        required=False,
        default="sn",
        metavar="SN",
        help="LDAP attribute for surname (e.g. sn)",
    )
    return parser.parse_args()


# =============================================================================
# Utilities
# =============================================================================

def build_ldap_url(fqdn_ldap: str) -> str:
    """Add a port to the LDAP URL when missing (389 for ldap, 636 for ldaps)."""
    fqdn_ldap = fqdn_ldap.strip()

    if fqdn_ldap.startswith("ldaps://"):
        default_port = 636
    elif fqdn_ldap.startswith("ldap://"):
        default_port = 389
    else:
        # No scheme provided: prefix with ldap://
        fqdn_ldap = f"ldap://{fqdn_ldap}"
        default_port = 389

    scheme_end = fqdn_ldap.find("://") + 3
    host_part = fqdn_ldap[scheme_end:]

    # Port already present?
    if re.search(r':\d+$', host_part):
        return fqdn_ldap

    return f"{fqdn_ldap}:{default_port}"


def info(msg: str) -> None:
    print(f"[INFO]  {msg}")


def ok(msg: str) -> None:
    print(f"[OK]    {msg}")


def error(msg: str) -> None:
    print(f"[ERROR] {msg}", file=sys.stderr)
    sys.exit(1)


# =============================================================================
# INI file manipulation (preserves comments)
# =============================================================================

_SECTION_RE = re.compile(r'^\s*\[(.+)\]\s*$')


def _set_key_in_section(lines: list, section: str, key: str, value: str) -> list:
    """
    Replace key=value inside [section]. If the key is missing, insert it just
    before the next section (or at end of file). If the section is missing,
    append it at the end of the file.
    """
    result = []
    in_section = False
    section_found = False
    key_set = False
    key_re = re.compile(r'^\s*' + re.escape(key) + r'\s*=')

    for line in lines:
        m = _SECTION_RE.match(line)
        if m:
            if m.group(1) == section:
                in_section = True
                section_found = True
                result.append(line)
                continue
            else:
                # Leaving the current section
                if in_section and not key_set:
                    result.append(f"{key} = {value}\n")
                    key_set = True
                in_section = False
                result.append(line)
                continue

        if in_section and key_re.match(line) and not key_set:
            result.append(f"{key} = {value}\n")
            key_set = True
            continue

        result.append(line)

    # End of file while still inside the target section
    if in_section and not key_set:
        result.append(f"{key} = {value}\n")
        key_set = True

    # Section not found: create it
    if not section_found:
        if result and result[-1] != "\n":
            result.append("\n")
        result.append(f"[{section}]\n")
        result.append(f"{key} = {value}\n")

    return result


def _add_value_to_method(lines: list, section: str, value_to_add: str) -> list:
    """
    In [section], add value_to_add to the `method = ...` line
    without removing existing values. Create the section/key if missing.
    """
    result = []
    in_section = False
    section_found = False
    method_handled = False
    method_re = re.compile(r'^\s*method\s*=')

    for line in lines:
        m = _SECTION_RE.match(line)
        if m:
            if m.group(1) == section:
                in_section = True
                section_found = True
                result.append(line)
                continue
            else:
                if in_section and not method_handled:
                    result.append(f"method = {value_to_add}\n")
                    method_handled = True
                in_section = False
                result.append(line)
                continue

        if in_section and method_re.match(line) and not method_handled:
            _, _, current_val = line.partition("=")
            existing = [v.strip() for v in current_val.split() if v.strip()]
            if value_to_add not in existing:
                existing.append(value_to_add)
            result.append(f"method = {' '.join(existing)}\n")
            method_handled = True
            continue

        result.append(line)

    # End of file while still inside the target section
    if in_section and not method_handled:
        result.append(f"method = {value_to_add}\n")
        method_handled = True

    # Missing section
    if not section_found:
        if result and result[-1] != "\n":
            result.append("\n")
        result.append(f"[{section}]\n")
        result.append(f"method = {value_to_add}\n")

    return result


# =============================================================================
# Entry point
# =============================================================================

def main() -> None:
    args = parse_args()
    filepath = Path(BASE_INI_PATH)

    if not filepath.exists():
        error(f"File not found: {filepath}")

    ldap_url = build_ldap_url(args.fqdn_ldap)

    # Backup
    backup = filepath.with_suffix(f".bak.{datetime.now().strftime('%Y%m%d_%H%M%S')}")
    shutil.copy2(filepath, backup)
    info(f"Backup: {backup}")

    lines = filepath.read_text(encoding="utf-8").splitlines(keepends=True)

    # [ldap]
    lines = _set_key_in_section(lines, "ldap", "baseDN", args.base_dn)

    # [authentication_externalldap]
    lines = _set_key_in_section(lines, "authentication_externalldap", "ldapurl",    ldap_url)
    lines = _set_key_in_section(lines, "authentication_externalldap", "suffix",     args.base_dn)
    lines = _set_key_in_section(lines, "authentication_externalldap", "bindname",   args.dn_bind)
    lines = _set_key_in_section(lines, "authentication_externalldap", "bindpasswd", args.pass_bind)
    lines = _set_key_in_section(lines, "authentication_externalldap", "attr",       args.attr)

    # [authentication] method += baseldap externalldap
    lines = _add_value_to_method(lines, "authentication", "baseldap")
    lines = _add_value_to_method(lines, "authentication", "externalldap")

    # [provisioning] method += externalldap
    lines = _add_value_to_method(lines, "provisioning", "externalldap")

    # [provisioning_externalldap]
    lines = _set_key_in_section(lines, "provisioning_externalldap", "exclude", "root")
    lines = _set_key_in_section(lines, "provisioning_externalldap", "ldap_uid", args.attr)
    lines = _set_key_in_section(lines, "provisioning_externalldap", "ldap_givenName", args.givenname)
    lines = _set_key_in_section(lines, "provisioning_externalldap", "ldap_sn", args.surname)

    filepath.write_text("".join(lines), encoding="utf-8")

    ok(f"File updated: {filepath}")
    ok(f"[ldap]                         baseDN     = {args.base_dn}")
    ok(f"[authentication_externalldap]  ldapurl    = {ldap_url}")
    ok(f"[authentication_externalldap]  suffix     = {args.base_dn}")
    ok(f"[authentication_externalldap]  bindname   = {args.dn_bind}")
    ok(f"[authentication_externalldap]  bindpasswd = {'*' * len(args.pass_bind)}")
    ok(f"[authentication_externalldap]  attr       = {args.attr}")
    ok(f"[authentication]               method    += baseldap externalldap")
    ok(f"[provisioning]                 method    += externalldap")
    ok(f"[provisioning_externalldap]    ldap_uid   = {args.attr}")
    ok(f"[provisioning_externalldap]    ldap_givenName = {args.givenname}")
    ok(f"[provisioning_externalldap]    ldap_sn   = {args.surname}")


if __name__ == "__main__":
    main()
