{ lib, cfg, models, allPhones, intercomEntries, mohDirs, greetingDirs }:

let
  hasTrunk         = cfg.sipTrunks != {};

  # Produces either "prefix<val>" (static) or a #exec that reads the file at runtime.
  # Use this wherever a config value can be supplied either inline or from a key file.
  runtimeLine = prefix: val: file:
    if file != null
      then ''#exec echo "${prefix}$(cat ${file})"''
      else "${prefix}${val}";

  hasSharedMailbox = cfg.sharedMailbox != null;

  hasPersonalMailboxes = lib.any (p: p.mailboxExt != null) (lib.attrValues allPhones);
  hasAnyMailbox        = hasPersonalMailboxes || hasSharedMailbox;

  # Helper to produce a literal Asterisk variable reference like ${EXTEN}
  # when used inside a Nix string interpolation: ${av "EXTEN"} → ${EXTEN}
  av = name: "\${${name}}";

  # Group phone keys by extension number (multiple phones can share one extension)
  phonesByExtension = lib.foldlAttrs (acc: key: phone:
    acc // { ${phone.extension} = (acc.${phone.extension} or []) ++ [ key ]; }
  ) {} allPhones;

  # Per-extension metadata derived from allPhones (all phones of the same extension
  # share the same personKey and mailboxExt by construction)
  extensionInfo = lib.mapAttrs (_ext: keys:
    let sample = allPhones.${lib.head keys}; in {
      inherit keys;
      personKey   = sample.personKey;
      mailboxExt  = sample.mailboxExt;
      ringTimeout =
        if sample.personKey != null
          then cfg.persons.${sample.personKey}.ringTimeout
          else 30;
    }
  ) phonesByExtension;

  # L2 only makes sense for person phones: they need separate lines for
  # personal vs family calls and have distinct mailbox MWI per line.
  # Shared phones (no personal mailbox) and sip-clients use L1 for everything.
  phoneHasL2 = key:
    let phone = allPhones.${key};
    in phone.personKey != null && phone.sharedLine && models.${phone.model}.hasMultiLine;

  # Dial target: L2 for provisioned phones, L1 for sip-clients (can only register once)
  dialTarget = key: if phoneHasL2 key then "PJSIP/${key}-l2" else "PJSIP/${key}";

  # Dial target strings
  # L2 endpoints for "all" DID routing: shared phones (always) + persons with sharedLine = true
  allL2Endpoints = lib.concatStringsSep "&"
    (map dialTarget
      (lib.attrNames (lib.filterAttrs (_: phone:
        phone.personKey == null || phone.sharedLine
      ) allPhones)));

  personL1Endpoints = personKey:
    lib.concatStringsSep "&"
      (map (key: "PJSIP/${key}")
        (lib.attrNames cfg.persons.${personKey}.phones));

  personsL2Endpoints = personKeys:
    lib.concatStringsSep "&"
      (lib.concatMap (personKey:
        map dialTarget (lib.attrNames cfg.persons.${personKey}.phones)
      ) personKeys);

  # --- Outbound trunk resolution ---
  # Find the first DID matching a predicate, returning { id, trunk } or null.
  findDid = pred:
    lib.foldlAttrs (acc: id: didCfg:
      if acc != null then acc
      else if pred didCfg then { inherit id; inherit (didCfg) trunk; }
      else null
    ) null cfg.dids;

  # The shared/family DID: routing.type = "all"
  allDid = findDid (d: d.routing.type == "all");

  # Personal DID for a given person key
  personDid = personKey: findDid (d: d.routing.type == "person" && d.routing.person == personKey);

  # Outbound assignment for an L1 endpoint: personal DID if person, else allDid
  l1OutboundFor = phone:
    if phone.personKey != null
      then let pd = personDid phone.personKey;
           in if pd != null then pd else allDid
      else allDid;

  # Render set_var lines for outbound trunk + caller ID, or empty string if no DID found
  outboundVars = did:
    if did == null then ""
    else
      let t = cfg.sipTrunks.${did.trunk}; in
      "set_var=OUTBOUND_TRUNK=trunk-${did.trunk}-endpoint\n    "
      + lib.optionalString (t.callerId != "" || t.callerIdFile != null)
          (runtimeLine "set_var=OUTBOUND_DID=" t.callerId t.callerIdFile + "\n    ");

  # Page: only intercom-capable phones with an intercom line (auto-answer speakerphone).
  allPageEndpoints = lib.concatStringsSep "&"
    (lib.mapAttrsToList (key: _: "PJSIP/${key}-intercom")
      (lib.filterAttrs (key: phone:
        models.${phone.model}.hasIntercom && cfg.intercomPrefix != null
      ) allPhones));

  # --- PJSIP endpoint generators ---

  genL1Endpoint = key: phone:
    let
      m           = models.${phone.model};
      # Personal mailbox takes priority; shared phones watch the shared mailbox
      effectiveMailbox =
        if phone.mailboxExt != null then phone.mailboxExt
        else if hasSharedMailbox then cfg.sharedMailbox.mailboxId
        else null;
      mailboxLine = lib.optionalString (effectiveMailbox != null)
        "mailboxes = ${effectiveMailbox}@voicemail\n    ";
      vmVar = lib.optionalString (effectiveMailbox != null)
        "set_var=VOICEMAIL_MAILBOX=${effectiveMailbox}\n    ";
      obVars = lib.optionalString hasTrunk (outboundVars (l1OutboundFor phone));
    in ''
    [${key}](${m.endpointTemplate})
    auth = auth-${key}
    aors = ${key}
    callerid = ${phone.displayName} <${phone.extension}>
    ${mailboxLine}${vmVar}${obVars}
    [auth-${key}](auth-userpass)
    username = ${key}
    password = ${phone.password}

    [${key}]
    type = aor
    max_contacts = ${toString m.maxContacts}
    remove_existing = yes
  '';

  genL2Endpoint = key: phone:
    let
      m           = models.${phone.model};
      k           = "${key}-l2";
      mailboxLine = lib.optionalString hasSharedMailbox
        "mailboxes = ${cfg.sharedMailbox.mailboxId}@voicemail\n    ";
      vmVar  = lib.optionalString hasSharedMailbox
        "set_var=VOICEMAIL_MAILBOX=${cfg.sharedMailbox.mailboxId}\n    ";
      obVars = outboundVars allDid;
    in ''
    [${k}](${m.endpointTemplate})
    auth = auth-${k}
    aors = ${k}
    ${mailboxLine}${vmVar}${obVars}
    [auth-${k}](auth-userpass)
    username = ${k}
    password = ${phone.password}

    [${k}]
    type = aor
    max_contacts = ${toString m.maxContacts}
    remove_existing = yes
  '';

  pjsip =
    ''
    [transport-tcp]
    type = transport
    protocol = tcp
    bind = 0.0.0.0:${toString cfg.sipPort}
    [transport-udp]
    type = transport
    protocol = udp
    bind = 0.0.0.0:${toString cfg.sipPort}
    ; --- endpoint templates ---

    ${lib.concatStringsSep "\n" (lib.mapAttrsToList (_: m: ''
    [${m.endpointTemplate}](!)
    type = endpoint
    context = internal
    transport = transport-tcp
    disallow = all
    ${lib.concatMapStrings (c: "allow = ${c}\n    ") m.codecs}
    direct_media = no
    send_pai = yes
    '') models)}

    [auth-userpass](!)
    type = auth
    auth_type = userpass

    ; --- phones (L1) ---

    ''
    + lib.concatStringsSep "\n" (lib.mapAttrsToList genL1Endpoint allPhones)

    + lib.optionalString hasTrunk (
        let provisionedPhones = lib.filterAttrs (key: _: phoneHasL2 key) allPhones; in
        lib.optionalString (provisionedPhones != {}) (
          "\n    ; --- family line endpoints (L2, provisioned phones only) ---\n\n"
          + lib.concatStringsSep "\n" (lib.mapAttrsToList genL2Endpoint provisionedPhones)
        )
      )

    + lib.concatMapStringsSep "\n" (ic: ''

    ; --- intercom ---
    [${ic.endpoint}](${ic.endpointTemplate})
    auth = auth-${ic.endpoint}
    aors = ${ic.endpoint}

    [auth-${ic.endpoint}](auth-userpass)
    username = ${ic.endpoint}
    password = ${ic.password}

    [${ic.endpoint}]
    type = aor
    max_contacts = ${toString ic.maxContacts}
    remove_existing = yes
    '') intercomEntries

    + lib.optionalString hasTrunk (
        "\n    ; --- SIP trunks ---\n"
        + lib.concatStringsSep "\n" (lib.mapAttrsToList (name: t: ''

    [trunk-${name}-registration]
    type = registration
    outbound_auth = trunk-${name}-auth
    ${runtimeLine "server_uri = sip:" t.host t.hostFile}
    ${let u = if t.usernameFile != null then "$(cat ${t.usernameFile})" else t.username;
           h = if t.hostFile     != null then "$(cat ${t.hostFile})"     else t.host;
       in if t.usernameFile != null || t.hostFile != null
            then ''#exec echo "client_uri = sip:${u}@${h}"''
            else "client_uri = sip:${u}@${h}"}
    retry_interval = 60
    forbidden_retry_interval = 600
    expiration = 3600

    [trunk-${name}-auth]
    type = auth
    auth_type = userpass
    ${runtimeLine "username = " t.username t.usernameFile}
    ${runtimeLine "password = " t.password t.passwordFile}

    [trunk-${name}-endpoint]
    type = endpoint
    context = from-external
    transport = transport-${t.transport}
    outbound_auth = trunk-${name}-auth
    aors = trunk-${name}-aor
    trust_id_inbound = yes
    disallow = all
    ${lib.concatMapStrings (c: "allow = ${c}\n    ") t.codecs}

    [trunk-${name}-aor]
    type = aor
    ${runtimeLine "contact = sip:" t.host t.hostFile}

    [trunk-${name}-identify]
    type = identify
    endpoint = trunk-${name}-endpoint
    ${runtimeLine "match = " t.host t.hostFile}
    '') cfg.sipTrunks));

  # --- Dialplan ---

  internalContext =
    "[internal]\n"
    # Line extensions (auto-generated from allPhones, grouped by extension)
    + lib.concatStringsSep "\n" (lib.mapAttrsToList (ext: info:
        let dialStr = lib.concatStringsSep "&" (map (k: "PJSIP/${k}") info.keys); in
        if info.mailboxExt != null
          then ''exten => ${ext},1,Dial(${dialStr},${toString info.ringTimeout})
 same => n,VoiceMail(${info.mailboxExt}@voicemail,u)''
          else "exten => ${ext},1,Dial(${dialStr},${toString info.ringTimeout})"
      ) extensionInfo)

    # BLF hints for person extensions
    + "\n" + lib.concatStringsSep "\n" (lib.mapAttrsToList (ext: info:
        lib.optionalString (info.personKey != null)
          "exten => ${ext},hint,${lib.concatStringsSep "&" (map (k: "PJSIP/${k}") info.keys)}"
      ) extensionInfo)

    # Page and app extensions from cfg.extensions
    + "\n" + lib.concatStringsSep "\n" (lib.mapAttrsToList (ext: extCfg:
        if      extCfg.mode == "page" then "exten => ${ext},1,Page(${allPageEndpoints},i,120)"
        else if extCfg.mode == "all"  then "exten => ${ext},1,Dial(${allL2Endpoints},30)"
        else                               "exten => ${ext},1,${extCfg.app}"
      ) cfg.extensions)

    # Auto-generated intercom extensions
    + "\n" + lib.concatMapStringsSep "\n" (ic:
        "exten => ${ic.extension},1,Dial(PJSIP/${ic.endpoint},30)"
      ) intercomEntries

    # Voicemail check (*97 — uses VOICEMAIL_MAILBOX set on the endpoint)
    + lib.optionalString hasAnyMailbox
        "\nexten => *97,1,VoiceMailMain(${av "VOICEMAIL_MAILBOX"}@voicemail,sa(0))"
    # Shared mailbox direct check extension
    + lib.optionalString hasSharedMailbox
        "\nexten => ${cfg.sharedMailbox.checkExtension},1,VoiceMailMain(${cfg.sharedMailbox.mailboxId}@voicemail,sa(0))"

    # Outbound — trunk and caller ID determined by set_var on the originating endpoint
    + lib.optionalString hasTrunk ''

exten => _0.,1,Set(CALLERID(num)=${av "OUTBOUND_DID"})
 same => n,Dial(PJSIP/${av "EXTEN"}@${av "OUTBOUND_TRUNK"})
exten => _00.,1,Set(CALLERID(num)=${av "OUTBOUND_DID"})
 same => n,Dial(PJSIP/${av "EXTEN"}@${av "OUTBOUND_TRUNK"})
exten => _+.,1,Set(CALLERID(num)=${av "OUTBOUND_DID"})
 same => n,Dial(PJSIP/${av "EXTEN"}@${av "OUTBOUND_TRUNK"})
'';

  externalContext = lib.optionalString hasTrunk (
    ''

[from-external]
; Provider sent INVITE without DID in Request-URI — extract from To header.
; To: <sip:+49123456789@provider.example.com:5060;user=phone>
;   CUT by ':' field 2 → +49123456789@provider.example.com
;   CUT by '@' field 1 → +49123456789
exten => s,1,Set(DID=${av "PJSIP_HEADER(read,To)"})
 same => n,Set(DID=${av "CUT(DID,:,2)"})
 same => n,Set(DID=${av "CUT(DID,@,1)"})
 same => n,Goto(from-external,${av "DID"},1)
 same => n,Hangup(21)
''
    + lib.concatStringsSep "\n" (lib.mapAttrsToList (id: didCfg:
        let
          r        = didCfg.routing;
          dialStr  =
            if      r.type == "all"    then allL2Endpoints
            else if r.type == "person" then personL1Endpoints r.person
            else                            personsL2Endpoints r.persons;
          mohOpt   = lib.optionalString (didCfg.musicOnHold != null) "m(${didCfg.musicOnHold})";
          dialArgs = "${dialStr},${toString r.timeout},${mohOpt}";
          noAnsLine =
            if didCfg.mailbox == "shared" then
              let gdir = greetingDirs.shared; in
              lib.optionalString (gdir != null)
                "\n same => n,Playback(${gdir}/greeting)"
              + "\n same => n,VoiceMail(${cfg.sharedMailbox.mailboxId}@voicemail,${if gdir != null then "s" else "u"})"
            else if didCfg.mailbox == "person" then
              let
                person = cfg.persons.${r.person};
                gdir   = greetingDirs.persons.${r.person};
              in
              lib.optionalString (gdir != null)
                "\n same => n,Playback(${gdir}/greeting)"
              + "\n same => n,VoiceMail(${person.extension}@voicemail,${if gdir != null then "s" else "u"})"
            else "";
          # Static number: inline it. Runtime number: #exec reads the file and outputs the exten line.
          # noAnsLine follows as static text — Asterisk processes #exec output inline with the file,
          # so "same =>" on the next line correctly refers to the exten => emitted by #exec.
          extenLine =
            if didCfg.numberFile != null
              then "#exec echo 'exten => '$(cat ${didCfg.numberFile})',1,Dial(${dialArgs})'"
              else "exten => ${didCfg.number},1,Dial(${dialArgs})";
        in
        "; ${didCfg.displayName} (${id}, trunk: ${didCfg.trunk})\n${extenLine}${noAnsLine}"
      ) cfg.dids)
  );

  extensions = internalContext + externalContext;

  rtp = ''
    [general]
    rtpstart = ${toString cfg.rtpStart}
    rtpend = ${toString cfg.rtpEnd}
  '';

  voicemail = ''
    [general]
    format = ulaw

    [voicemail]
  '' + lib.concatStringsSep "\n" (
      lib.mapAttrsToList (_key: person:
        lib.optionalString person.mailbox
          "  ${person.extension} => ,${person.displayName},,attach=no"
      ) cfg.persons
      ++ lib.optional hasSharedMailbox
          "  ${cfg.sharedMailbox.mailboxId} => ,${cfg.sharedMailbox.displayName},,attach=no"
    );

  musiconhold = lib.optionalString (cfg.mohClasses != {}) (
    "[general]\n"
    + lib.concatStringsSep "\n" (lib.mapAttrsToList (name: cls: ''
    [${name}]
    mode = files
    directory = ${mohDirs.${name}}
    sort = ${cls.sort}
    '') cfg.mohClasses)
  );

in {
  "pjsip.conf"      = pjsip;
  "extensions.conf" = extensions;
  "rtp.conf"        = rtp;
} // lib.optionalAttrs hasAnyMailbox {
  "voicemail.conf" = voicemail;
} // lib.optionalAttrs (cfg.mohClasses != {}) {
  "musiconhold.conf" = musiconhold;
}
