{ lib, cfg, models, allPhones }:

# Provisioned sharedPhones require a MAC address key
(lib.mapAttrsToList (key: phone: {
  assertion = !models.${phone.model}.hasProvisioning || builtins.match "[0-9a-f]{12}" key != null;
  message   = "services.voip: sharedPhone \"${key}\" (model ${phone.model}) requires a lowercase 12-char hex MAC address as key";
}) cfg.sharedPhones)
++
# Provisioned sharedPhones require a non-empty label
(lib.mapAttrsToList (key: phone: {
  assertion = !models.${phone.model}.hasProvisioning || phone.label != "";
  message   = "services.voip: sharedPhone \"${key}\" (model ${phone.model}) requires a non-empty label";
}) cfg.sharedPhones)
++
# Provisioned person phones require a MAC address key
(lib.concatLists (lib.mapAttrsToList (personKey: person:
  lib.mapAttrsToList (key: phone: {
    assertion = !models.${phone.model}.hasProvisioning || builtins.match "[0-9a-f]{12}" key != null;
    message   = "services.voip: persons.${personKey}.phones.\"${key}\" (model ${phone.model}) requires a lowercase 12-char hex MAC address as key";
  }) person.phones
) cfg.persons))
++
# Provisioned person phones require a non-empty label
(lib.concatLists (lib.mapAttrsToList (personKey: person:
  lib.mapAttrsToList (key: phone: {
    assertion = !models.${phone.model}.hasProvisioning || phone.label != "";
    message   = "services.voip: persons.${personKey}.phones.\"${key}\" (model ${phone.model}) requires a non-empty label";
  }) person.phones
) cfg.persons))
++
# No duplicate phone keys across sharedPhones and persons.*.phones
[{
  assertion =
    let keys = lib.attrNames allPhones;
    in lib.length keys == lib.length (lib.unique keys);
  message = "services.voip: duplicate phone key detected across sharedPhones and persons.*.phones";
}]
++
# No duplicate extensions across sharedPhones and persons
[{
  assertion =
    let
      exts = (lib.mapAttrsToList (_: p: p.extension) cfg.sharedPhones)
          ++ (lib.mapAttrsToList (_: p: p.extension) cfg.persons);
    in lib.length exts == lib.length (lib.unique exts);
  message = "services.voip: duplicate extension number across sharedPhones and persons";
}]
++
# dids require at least one sipTrunk
(lib.optionals (cfg.dids != {}) [{
  assertion = cfg.sipTrunks != {};
  message   = "services.voip: dids are configured but sipTrunks is empty";
}])
++
# each DID must reference an existing trunk
(lib.mapAttrsToList (did: didCfg: {
  assertion = lib.hasAttr didCfg.trunk cfg.sipTrunks;
  message   = "services.voip: DID ${did} references trunk \"${didCfg.trunk}\" which is not in services.voip.sipTrunks";
}) cfg.dids)
++
# dids with mailbox="shared" require sharedMailbox
(lib.mapAttrsToList (did: didCfg: {
  assertion = didCfg.mailbox != "shared" || cfg.sharedMailbox != null;
  message   = "services.voip: DID ${did} has mailbox=\"shared\" but sharedMailbox is not configured";
}) cfg.dids)
++
# dids with mailbox="person" require routing.type="person"
(lib.mapAttrsToList (did: didCfg: {
  assertion = didCfg.mailbox != "person" || didCfg.routing.type == "person";
  message   = "services.voip: DID ${did} has mailbox=\"person\" but routing.type is not \"person\"";
}) cfg.dids)
++
# dids routing.type="person" — person key must be non-empty
(lib.mapAttrsToList (did: didCfg: {
  assertion = didCfg.routing.type != "person" || didCfg.routing.person != "";
  message   = "services.voip: DID ${did} has routing.type=\"person\" but routing.person is not set";
}) cfg.dids)
++
# dids routing.type="person" — referenced person must exist
(lib.mapAttrsToList (did: didCfg: {
  assertion = didCfg.routing.type != "person" || didCfg.routing.person == "" || lib.hasAttr didCfg.routing.person cfg.persons;
  message   = "services.voip: DID ${did} references person \"${didCfg.routing.person}\" which is not in services.voip.persons";
}) cfg.dids)
++
# dids routing.type="persons" — persons list must be non-empty
(lib.mapAttrsToList (did: didCfg: {
  assertion = didCfg.routing.type != "persons" || didCfg.routing.persons != [];
  message   = "services.voip: DID ${did} has routing.type=\"persons\" but routing.persons is empty";
}) cfg.dids)
++
# dids routing.type="persons" — all referenced persons must exist
(lib.concatLists (lib.mapAttrsToList (did: didCfg:
  lib.optionals (didCfg.routing.type == "persons")
    (map (p: {
      assertion = lib.hasAttr p cfg.persons;
      message   = "services.voip: DID ${did} references person \"${p}\" which is not in services.voip.persons";
    }) didCfg.routing.persons)
) cfg.dids))
++
# dids musicOnHold must reference an existing mohClass
(lib.concatLists (lib.mapAttrsToList (did: didCfg:
  lib.optional (didCfg.musicOnHold != null) {
    assertion = lib.hasAttr didCfg.musicOnHold cfg.mohClasses;
    message   = "services.voip: DID ${did} references mohClass \"${didCfg.musicOnHold}\" which is not in services.voip.mohClasses";
  }
) cfg.dids))
++
# sipTrunks: each required field needs either a literal or a file
(lib.concatLists (lib.mapAttrsToList (name: t: [
  { assertion = t.host     != "" || t.hostFile     != null;
    message   = "services.voip: sipTrunks.\"${name}\" requires host or hostFile"; }
  { assertion = t.username != "" || t.usernameFile != null;
    message   = "services.voip: sipTrunks.\"${name}\" requires username or usernameFile"; }
  { assertion = t.password != "" || t.passwordFile != null;
    message   = "services.voip: sipTrunks.\"${name}\" requires password or passwordFile"; }
]) cfg.sipTrunks))
++
# dids: each DID needs a number either inline or from a file
(lib.mapAttrsToList (id: d: {
  assertion = d.number != "" || d.numberFile != null;
  message   = "services.voip: dids.\"${id}\" requires number or numberFile";
}) cfg.dids)
++
# extensions with mode="app" must have a non-null app field
(lib.mapAttrsToList (ext: extCfg: {
  assertion = extCfg.mode != "app" || extCfg.app != null;
  message   = "services.voip: extension \"${ext}\" has mode=\"app\" but app is not set";
}) cfg.extensions)
++
# intercomPrefix must not collide with any declared extension
(lib.optionals (cfg.intercomPrefix != null)
  (lib.mapAttrsToList (_key: phone:
    let ext = "${cfg.intercomPrefix}${phone.extension}"; in {
      assertion = !lib.hasAttr ext cfg.extensions;
      message   = "services.voip: auto-generated intercom extension \"${ext}\" collides with a declared extension; rename it or change intercomPrefix";
    }
  ) allPhones))
