#!/usr/bin/env python3

# Copyright 2023-2025 Ping Identity Corporation. All Rights Reserved
#
# This code is to be used exclusively in connection with Ping Identity
# Corporation software or services. Ping Identity Corporation only offers
# such software or services to legal entities who have entered into a
# binding license agreement with Ping Identity Corporation.

import os
from datetime import datetime
import random
import gzip  # Using this is 100% slower
import argparse
import importlib.util
from numpy import random as nr
from pathlib import Path

try:
    importlib.util.find_spec("fastuuid")
    import fastuuid as myuuid
except ImportError:
    import uuid as myuuid  # This is 100% slower than fastuuid

parser = argparse.ArgumentParser(description="Generate ldif files to load via ldapmodify or import-ldif.")
parser.add_argument("-n", "--numusers", dest="numusers", default=1000, help="Total number of users to generate.")
parser.add_argument("-x", "--offset", dest="offset", default=0, help="Start sequence of numusers from here.")
parser.add_argument("-r", "--realm", dest="realm", default="alpha", help='AM Realm. Default is "alpha".')
parser.add_argument(
    "-c",
    "--compress",
    dest="compress",
    action="store_true",
    help="Inline compress ldif file. Note this is 100 percent slower.",
)
parser.add_argument("-p", "--parents", dest="parents", action="store_true", help="Generate parent branches also.")
parser.add_argument(
    "-f",
    "--names-file",
    dest="names_file_path",
    default="./names.txt",
    help="Path to file containing a list of names for last and first.",
)
parser.add_argument("-o", "--out-dir", dest="output_directory", default="./", help="Output directory of ldif files.")
parser.add_argument("-m", "--cdm", dest="cdm", action="store_true", help="This mode for CDM compatibility.")
parser.add_argument("-s", "--specref", dest="specref", action="store_true", help="Genarate SpecRef data only")
parser.add_argument(
    "--streets-file",
    dest="streets_file_path",
    default="./streets.txt",
    help="Path to the file containing list of streets.",
)
parser.add_argument(
    "--states-file", dest="states_file_path", default="./states.txt", help="Path to the file containing list of states."
)
parser.add_argument(
    "--cities-file", dest="cities_file_path", default="./cities.txt", help="Path to the file containing list of cities."
)
args = parser.parse_args()

numusers = int(args.numusers)
offset = int(args.offset)
realm = args.realm
compress = args.compress
names_file_path = args.names_file_path
streets_file_path = args.streets_file_path
states_file_path = args.states_file_path
cities_file_path = args.cities_file_path
parents = args.parents
output_directory = args.output_directory
is_cdm = args.cdm
is_specref = args.specref

built_in_names = False
person_suffix = None
meta_suffix = None
relationship_suffix = None

if is_cdm:
    person_suffix = "ou=people,ou=identities"
    meta_suffix = "ou=usermeta,ou=internal,dc=openidm,dc=forgerock,dc=io"
    relationship_suffix = "ou=relationships,dc=openidm,dc=forgerock,dc=io"
else:
    person_suffix = f"ou=user,o={realm},o=root,ou=identities"
    meta_suffix = f"ou=usermeta,o={realm},o=root,ou=identities"
    relationship_suffix = "ou=relationships,dc=openidm,dc=example,dc=com"


def telnum():
    val = nr.randint(99999999999)
    return val


def timestamp():
    return datetime.now().strftime("%Y%m%d%H%M%S.777Z")


def person(seq, puid, muid, ruid, ldif):
    if built_in_names:
        first = f"first{seq}"
        last = f"last{seq}"
    else:  # use names.txt
        first = random.choice(names)
        last = random.choice(names)
    cn = first + " " + last
    uid = "user." + str(seq)
    tel = telnum()
    rev = myuuid.uuid4()
    initials = first[0] + last[0]
    postalCode = random.randrange(10000, 99999, 1)
    iam_disable_flag = random.choice(["true", "false"])
    locked_profile_flag = random.choice(["true", "false"])
    enterprise_migration_code = random.randrange(1000, 9999, 1)
    user_type_code = random.choice(["fulltime", "parttime", "contractor"])
    language_code = random.choice(["EN", "FR", "DE", "ES"])
    mobl_intl_call_prefix_code = random.randrange(10, 99, 1)
    ph_intl_call_prefix_code = random.randrange(100, 999, 1)
    company_name = random.choice(names)
    mobile = telnum()
    street = random.choice(streets) + ", " + str(random.randrange(1, 999, 1))
    state = random.choice(states)
    country = random.choice(
        [
            "Yugoslavia",
            "France",
            "Spain",
            "United Kingdom",
            "United States",
            "Germany",
            "Pakistan",
            "India",
            "Czech Republic",
        ]
    )
    city = random.choice(cities)

    ldif.write(f"\ndn: fr-idm-uuid={puid},{person_suffix}\n")
    ldif.write(
        """objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: iplanet-am-user-service
objectClass: iplanet-am-auth-configuration-service
objectClass: iplanet-am-managed-person
objectClass: iPlanetPreferences
objectClass: devicePrintProfilesContainer
objectClass: kbaInfoContainer
objectClass: inetuser
objectClass: oathDeviceProfilesContainer
objectClass: pushDeviceProfilesContainer
objectClass: sunAMAuthAccountLockout
objectClass: sunFMSAML2NameIdentifier
objectClass: webauthnDeviceProfilesContainer
objectClass: forgerock-am-dashboard-service
objectClass: fr-idm-managed-user-explicit"""
    )
    # There are some subtle differences between IDC and CDM hence
    # place any divergent attributes here
    if is_cdm:
        ldif.write(
            "\nobjectClass: fr-idm-managed-user-hybrid-obj\n"
            "fr-idm-managed-user-custom-attrs: {}\n"
            'fr-idm-managed-user-meta: {"firstResourceCollection":"managed/user", \
           "firstResourceId":"%s", "firstPropertyName":"_meta", \
           "secondResourceCollection":"internal/usermeta","secondResourceId":"%s", \
           "secondPropertyName":null,"properties":null,"_rev":"%s","_id":"%s"}uid=%s,%s\n'
            % (puid, muid, rev, ruid, muid, meta_suffix)
        )
    if not is_cdm:
        ldif.write(
            "\nobjectClass: fraas-admin\n"
            "objectClass: fr-ext-attrs\n"
            "objectClass: fr-idm-hybrid-obj\n"
            "objectClass: deviceProfilesContainer\n"
            "fr-idm-custom-attrs: {}\n"
            'fr-idm-managed-user-meta: {"firstResourceCollection":"managed/%s_user", \
           "firstResourceId":"%s", "firstPropertyName":"_meta", \
           "secondResourceCollection":"managed/%s_usermeta","secondResourceId":"%s", \
           "secondPropertyName":null,"properties":null,"_rev":"%s","_id":"%s"}uid=%s,%s\n'
            % (realm, puid, realm, muid, rev, ruid, muid, meta_suffix)
        )
    # End
    ldif.write(
        f"fr-idm-uuid: {puid}\n"
        f"mail: user.{seq}@example.com\n"
        f"cn: {cn}\n"
        f"givenName: {first}\n"
        f"sn: {last}\n"
        f"telephoneNumber: {tel}\n"
        f"uid: {uid}\n"
        "userPassword: Pa_ssw0rd\n"
        "inetUserStatus: active\n"
        f"co: {country}\n"
        f"l: {city}\n"
        f"st: {state}\n"
        f"street: {street}\n"
        f"fr-attr-date1: {timestamp()}\n"
        f"fr-attr-date2: {timestamp()}\n"
        f"fr-attr-idate1: {timestamp()}\n"
        f"fr-attr-idate2: {timestamp()}\n"
        f"fr-attr-idate3: {timestamp()}\n"
        f"fr-attr-idate4: {timestamp()}\n"
        f"fr-attr-iint1: {mobile}\n"
        f"fr-attr-imulti1: {company_name}\n"
        f"fr-attr-int1: {ph_intl_call_prefix_code}\n"
        f"fr-attr-int2: {mobl_intl_call_prefix_code}\n"
        f"fr-attr-istr1: {puid}\n"
        f"fr-attr-istr2: {language_code}\n"
        f"fr-attr-istr4: {user_type_code}\n"
        f"fr-attr-istr5: {enterprise_migration_code}\n"
        f"fr-attr-multi4: {locked_profile_flag}\n"
        f"fr-attr-multi5: {iam_disable_flag}\n"
        f"fr-attr-str1: {random.choice(streets)}\n"
        f"fr-attr-str2: {initials}\n"
        f"fr-attr-str3: {cn} {initials} {postalCode}\n"
        f"fr-attr-str5: {random.choice(streets)} Ltd.\n"
    )


def usermeta(muid, ldif):
    cdate = datetime.now().strftime("%Y-%m-%dT%X.%fZ")  # 2021-09-01T17:18:33.639825Z

    ldif.write(
        f"\ndn: uid={muid},{meta_suffix}\n"
        "objectClass: top\n"
        "objectClass: uidObject\n"
        "objectClass: fr-idm-generic-obj\n"
        f"uid: {muid}\n"
        'fr-idm-json: {"createDate":"%s","lastChanged":{"date":"%s"},"loginCount":0}\n' % (cdate, cdate)
    )


def relationship(ruid, puid, muid, ldif):
    """Build the relationhip entry. Note this is not needed in SpecRef IDM"""

    if is_cdm:
        ldif.write(
            f"\ndn: uid={ruid},{relationship_suffix}\n"
            "objectClass: top\n"
            "objectClass: uidObject\n"
            "objectClass: fr-idm-relationship\n"
            f"uid: {ruid}\n"
            'fr-idm-relationship-json: {"firstResourceCollection":"managed/user","firstResourceId":"%s", \
           "firstPropertyName":"_meta","secondResourceCollection":"internal/usermeta", \
           "secondResourceId":"%s","secondPropertyName":null,"properties":null}\n'
            % (puid, muid)
        )

    if not is_cdm:
        ldif.write(
            f"\ndn: uid={ruid},{relationship_suffix}\n"
            "objectClass: top\n"
            "objectClass: uidObject\n"
            "objectClass: fr-idm-relationship\n"
            f"uid: {ruid}\n"
            'fr-idm-relationship-json: {"firstResourceCollection":"managed/%s_user","firstResourceId":"%s", \
                   "firstPropertyName":"_meta","secondResourceCollection":"managed/%s_usermeta", \
                   "secondResourceId":"%s","secondPropertyName":null,"properties":null}\n'
            % (realm, puid, realm, muid)
        )


def branches(ldif):
    ldif.write(
        """dn: ou=identities
objectClass: top
objectClass: organizationalUnit

dn: o=root,ou=identities
objectClass: top
objectClass: organization
o: root

dn: o=%s,o=root,ou=identities
objectClass: top
objectClass: organization
o: alpha

dn: ou=user,o=%s,o=root,ou=identities
objectClass: top
objectClass: organizationalUnit
ou: user

dn: ou=usermeta,o=%s,o=root,ou=identities
objectClass: top
objectClass: organizationalUnit
ou: usermeta\n"""
        % (realm, realm, realm)
    )


if Path(names_file_path).is_file():
    with open(names_file_path) as file:
        names = file.read().splitlines()
    file.close()
else:
    built_in_names = True

if Path(streets_file_path).is_file:
    with open(streets_file_path) as file:
        streets = file.read().splitlines()
    file.close()

if Path(states_file_path).is_file:
    with open(states_file_path) as file:
        states = file.read().splitlines()
    file.close()

if Path(cities_file_path).is_file:
    with open(cities_file_path) as file:
        cities = file.read().splitlines()
    file.close()


def main():
    amidstore_ldif = None
    idmrepo_ldif = None
    primary_ldif_name = "amIdentityStore.ldif"
    secondary_ldif_name = "idmRepo.ldif"

    if not os.path.exists(output_directory):
        os.mkdir(output_directory)

    if compress:
        amidstore_ldif = gzip.open(f"{output_directory}/{primary_ldif_name}.gz", "wt")
        if not is_specref:
            idmrepo_ldif = gzip.open(f"{output_directory}/{secondary_ldif_name}.gz", "w")
    else:
        amidstore_ldif = open(f"{output_directory}/{primary_ldif_name}", "w")
        if not is_specref:
            idmrepo_ldif = open(f"{output_directory}/{secondary_ldif_name}", "wt")

    if parents:
        branches(amidstore_ldif)

    for i in range(offset, numusers + offset):
        puid = myuuid.uuid4()
        muid = myuuid.uuid4()
        ruid = myuuid.uuid4()

        person(i, puid, muid, ruid, amidstore_ldif)
        if is_cdm:
            usermeta(muid, idmrepo_ldif)
        else:
            usermeta(muid, amidstore_ldif)

        if not is_specref:
            relationship(ruid, puid, muid, idmrepo_ldif)

    if not is_specref:
        idmrepo_ldif.close()

    amidstore_ldif.close()


if __name__ == "__main__":
    main()
