<?php

namespace App\Console\Commands;

use App\Services\DataverseService;
use Illuminate\Console\Command;
use Throwable;

/**
 * Seeds cr6f2_appuser from the existing systemuser-based identity so that
 * enabling IDENTITY_APPUSERS doesn't blank anyone:
 *   1. one cr6f2_appuser row per systemuser that appears in cr6f2_companypicmap
 *      (provider=microsoft, role tier copied from their null-company "role row",
 *      cr6f2_SystemUser bridged),
 *   2. stamps cr6f2_AppUser on every companypicmap row (so app-user-scoped reads
 *      resolve the same companies as the legacy systemuser filter).
 *
 * Requires identity:provision-schema to have run (--apply) first. Default is a
 * DRY RUN; pass --apply to write. Idempotent: existing app-users (matched by
 * email) are reused, already-stamped membership rows are skipped.
 */
class SeedAppUsers extends Command
{
    protected $signature = 'identity:seed-appusers
                            {--apply : Actually create app-users / stamp memberships (default: dry run)}';

    protected $description = 'Seed cr6f2_appuser from existing systemusers + PIC-map role rows (dry run unless --apply).';

    public function handle(DataverseService $dv): int
    {
        $apply = (bool) $this->option('apply');
        $f     = config('identity.fields');
        $table = config('identity.table');

        $this->line('  <fg=cyan>Mode:</> ' . ($apply ? 'APPLY' : 'dry run'));
        $this->newLine();

        // 1) All PIC-map rows (role rows + membership rows). Select the app-user
        //    lookup too — but tolerate its absence so a dry run works before the
        //    provisioner has added the column.
        $membershipVal = config('identity.membership_val');
        $baseSelect    = ['cr6f2_companypicmapid', '_cr6f2_picuser_value', '_cr6f2_company_value', 'cr6f2_type', 'cr6f2_isadmin', 'cr6f2_isdeveloper'];
        $hasMembershipCol = true;
        try {
            $rows = $dv->get('cr6f2_companypicmaps', [
                'select' => array_merge($baseSelect, [$membershipVal]),
                'top'    => 5000,
            ])['value'] ?? [];
        } catch (Throwable) {
            $hasMembershipCol = false;
            $this->warn('cr6f2_AppUser lookup not found yet — run identity:provision-schema --apply first. Treating all memberships as unstamped.');
            try {
                $rows = $dv->get('cr6f2_companypicmaps', ['select' => $baseSelect, 'top' => 5000])['value'] ?? [];
            } catch (Throwable $e) {
                $this->error('Failed to read cr6f2_companypicmaps: ' . $e->getMessage());
                return Command::FAILURE;
            }
        }

        // 2) Distinct systemusers + their authoritative role tier (from the
        //    null-company role row; else legacy booleans; else normal).
        $bySystemUser = [];   // guid => ['type'=>int]
        foreach ($rows as $r) {
            $guid = $r['_cr6f2_picuser_value'] ?? null;
            if (! $guid) {
                continue;
            }
            $bySystemUser[$guid] ??= ['type' => null];
            $isRoleRow = empty($r['_cr6f2_company_value']);
            $type      = $r['cr6f2_type'] ?? null;
            if ($isRoleRow && $type !== null) {
                $bySystemUser[$guid]['type'] = (int) $type;
            }
            if ($bySystemUser[$guid]['type'] === null) {
                if (! empty($r['cr6f2_isdeveloper'])) $bySystemUser[$guid]['type'] = config('identity.role_types.developer');
                elseif (! empty($r['cr6f2_isadmin']))  $bySystemUser[$guid]['type'] = config('identity.role_types.admin');
            }
        }
        if (! $bySystemUser) {
            $this->warn('No PIC-map rows with a systemuser — nothing to seed.');
            return Command::SUCCESS;
        }

        // 3) systemuser → {email, fullname}.
        $identities = $this->fetchSystemUsers($dv, array_keys($bySystemUser));

        // 4) Upsert one app-user per systemuser; build guid → appUserId map.
        $guidToAppUser = [];
        $created = 0; $reused = 0; $skippedNoEmail = 0;
        foreach ($bySystemUser as $guid => $info) {
            $ident = $identities[$guid] ?? null;
            $email = strtolower(trim((string) ($ident['email'] ?? '')));
            $name  = $ident['fullname'] ?? $email;
            $type  = $info['type'] ?? config('identity.role_types.normal');

            if ($email === '') {
                $this->line("  <fg=red>·</> systemuser {$guid} — no email, skipped");
                $skippedNoEmail++;
                continue;
            }

            $existing = $this->findAppUserByEmail($dv, $email);
            if ($existing) {
                $guidToAppUser[$guid] = $existing;
                $reused++;
                $this->line("  <fg=gray>·</> {$email} — app-user exists ({$existing})");
                continue;
            }

            if (! $apply) {
                $this->line("  <fg=green>+</> {$email} — would create app-user (type={$type})");
                $created++;
                continue;
            }

            try {
                $payload = [
                    $f['email']     => $email,
                    $f['name']      => $name,
                    $f['provider']  => 'microsoft',
                    $f['type']      => $type,
                    $f['is_active'] => true,
                    "{$f['systemuser']}@odata.bind" => "/systemusers({$guid})",
                ];
                $id = $dv->create($table, $payload);
                $guidToAppUser[$guid] = $id;
                $created++;
                $this->line("  <fg=green>✓</> {$email} — app-user created ({$id})");
            } catch (Throwable $e) {
                $this->error("  ✗ {$email}: " . $e->getMessage());
                return Command::FAILURE;
            }
        }

        // 5) Stamp cr6f2_AppUser on membership rows that don't have it yet.
        $membershipNav = config('identity.membership_nav');
        $stamped = 0; $alreadyStamped = 0; $noAppUser = 0;
        foreach ($rows as $r) {
            $guid = $r['_cr6f2_picuser_value'] ?? null;
            $mapId = $r['cr6f2_companypicmapid'] ?? null;
            if (! $guid || ! $mapId) {
                continue;
            }
            if ($hasMembershipCol && ! empty($r[$membershipVal])) {
                $alreadyStamped++;
                continue;
            }
            $appUserId = $guidToAppUser[$guid] ?? null;
            if (! $appUserId) {
                $noAppUser++;
                continue; // no email / created next apply-run
            }
            if (! $apply) {
                $stamped++;
                continue;
            }
            try {
                $dv->update('cr6f2_companypicmaps', $mapId, [
                    "{$membershipNav}@odata.bind" => "/{$table}({$appUserId})",
                ]);
                $stamped++;
            } catch (Throwable $e) {
                $this->error("  ✗ stamp map {$mapId}: " . $e->getMessage());
                return Command::FAILURE;
            }
        }

        // 6) Stamp each app-user's OCR account (cr6f2_OcrCreditAccount) from the
        //    account of the companies they belong to, so existing users show under
        //    their account instead of the unassigned pool. Users with no companies
        //    (platform-level super-admin/dev) are left unassigned. Tolerant of a
        //    missing column — run identity:provision-schema --apply first.
        $acctNav = config('identity.fields.account');
        $companyAccount = []; // companyId => accountId
        try {
            foreach (($dv->get('cr6f2_companies', [
                'select' => ['cr6f2_companyid', '_cr6f2_ocrcreditaccountmap_value'],
                'top'    => 2000,
            ])['value'] ?? []) as $c) {
                if (! empty($c['cr6f2_companyid'])) {
                    $companyAccount[$c['cr6f2_companyid']] = $c['_cr6f2_ocrcreditaccountmap_value'] ?? null;
                }
            }
        } catch (Throwable) {
            // leave empty → account stamping is skipped
        }
        $userAccount = []; // systemuser guid => accountId (first company's account wins)
        foreach ($rows as $r) {
            $guid = $r['_cr6f2_picuser_value'] ?? null;
            $comp = $r['_cr6f2_company_value'] ?? null;
            if (! $guid || ! $comp || isset($userAccount[$guid])) {
                continue;
            }
            if ($acct = ($companyAccount[$comp] ?? null)) {
                $userAccount[$guid] = $acct;
            }
        }
        $acctStamped = 0; $acctNone = 0; $acctColMissing = false;
        foreach ($guidToAppUser as $guid => $appUserId) {
            $acct = $userAccount[$guid] ?? null;
            if (! $acct)   { $acctNone++; continue; }
            if (! $apply)  { $acctStamped++; continue; }
            try {
                $dv->update($table, $appUserId, ["{$acctNav}@odata.bind" => "/crf61_cr6f2_ocrcreditses({$acct})"]);
                $acctStamped++;
            } catch (Throwable) {
                $acctColMissing = true;
                break;
            }
        }
        if ($acctColMissing) {
            $this->warn('  cr6f2_OcrCreditAccount not found — run identity:provision-schema --apply, then re-run to stamp accounts.');
        }

        $this->newLine();
        $verb = $apply ? '' : 'would ';
        $this->info("Done. app-users: {$verb}create {$created}, reused {$reused}, no-email {$skippedNoEmail}. "
            . "memberships: {$verb}stamp {$stamped}, already {$alreadyStamped}, pending {$noAppUser}. "
            . "accounts: {$verb}stamp {$acctStamped}, unassigned {$acctNone}.");
        if (! $apply) {
            $this->info('Dry run — re-run with --apply to write.');
        }
        return Command::SUCCESS;
    }

    /** @param string[] $guids  @return array<string,array{email:?string,fullname:?string}> */
    private function fetchSystemUsers(DataverseService $dv, array $guids): array
    {
        $out = [];
        foreach (array_chunk(array_values(array_unique($guids)), 20) as $chunk) {
            $clause = '(' . implode(' or ', array_map(fn ($g) => "systemuserid eq {$g}", $chunk)) . ')';
            try {
                $res = $dv->get('systemusers', [
                    'select' => ['systemuserid', 'internalemailaddress', 'fullname'],
                    'filter' => $clause,
                    'top'    => count($chunk),
                ])['value'] ?? [];
            } catch (Throwable) {
                $res = [];
            }
            foreach ($res as $u) {
                $out[$u['systemuserid']] = ['email' => $u['internalemailaddress'] ?? null, 'fullname' => $u['fullname'] ?? null];
            }
        }
        return $out;
    }

    private function findAppUserByEmail(DataverseService $dv, string $email): ?string
    {
        $f    = config('identity.fields');
        $safe = str_replace("'", "''", $email);
        try {
            $res = $dv->get(config('identity.table'), [
                'select' => [$f['id']],
                'filter' => "{$f['email']} eq '{$safe}'",
                'top'    => 1,
            ])['value'] ?? [];
            return $res[0][$f['id']] ?? null;
        } catch (Throwable) {
            return null;
        }
    }
}
