<?php

namespace App\Models;

use App\Support\Concerns\DerivesWorkspaceIdFromTenant;
use App\Support\Artifacts\ArtifactProviderDetail;
use App\Support\Artifacts\ArtifactSourceDescriptor;
use App\Support\Artifacts\ArtifactSourceResolver;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Arr;

class Finding extends Model
{
    /** @use HasFactory<\Database\Factories\FindingFactory> */
    use DerivesWorkspaceIdFromTenant;

    use HasFactory;

    public const string FINDING_TYPE_DRIFT = 'drift';

    public const string FINDING_TYPE_PERMISSION_POSTURE = 'permission_posture';

    public const string FINDING_TYPE_ENTRA_ADMIN_ROLES = 'entra_admin_roles';

    public const string SEVERITY_LOW = 'low';

    public const string SEVERITY_MEDIUM = 'medium';

    public const string SEVERITY_HIGH = 'high';

    public const string SEVERITY_CRITICAL = 'critical';

    public const string STATUS_NEW = 'new';

    public const string STATUS_TRIAGED = 'triaged';

    public const string STATUS_IN_PROGRESS = 'in_progress';

    public const string STATUS_REOPENED = 'reopened';

    public const string STATUS_RESOLVED = 'resolved';

    public const string STATUS_CLOSED = 'closed';

    public const string STATUS_RISK_ACCEPTED = 'risk_accepted';

    public const string RESOLVE_REASON_REMEDIATED = 'remediated';

    public const string RESOLVE_REASON_NO_LONGER_DRIFTING = 'no_longer_drifting';

    public const string RESOLVE_REASON_PERMISSION_GRANTED = 'permission_granted';

    public const string RESOLVE_REASON_PERMISSION_REMOVED_FROM_REGISTRY = 'permission_removed_from_registry';

    public const string RESOLVE_REASON_ROLE_ASSIGNMENT_REMOVED = 'role_assignment_removed';

    public const string RESOLVE_REASON_GA_COUNT_WITHIN_THRESHOLD = 'ga_count_within_threshold';

    public const string CLOSE_REASON_FALSE_POSITIVE = 'false_positive';

    public const string CLOSE_REASON_DUPLICATE = 'duplicate';

    public const string CLOSE_REASON_NO_LONGER_APPLICABLE = 'no_longer_applicable';

    public const string CLOSE_REASON_ACCEPTED_RISK = 'accepted_risk';

    public const string REOPEN_REASON_RECURRED_AFTER_RESOLUTION = 'recurred_after_resolution';

    public const string REOPEN_REASON_VERIFICATION_FAILED = 'verification_failed';

    public const string REOPEN_REASON_MANUAL_REASSESSMENT = 'manual_reassessment';

    public const string RESPONSIBILITY_STATE_ORPHANED_ACCOUNTABILITY = 'orphaned_accountability';

    public const string RESPONSIBILITY_STATE_OWNED_UNASSIGNED = 'owned_unassigned';

    public const string RESPONSIBILITY_STATE_ASSIGNED = 'assigned';

    protected $guarded = [];

    protected $casts = [
        'acknowledged_at' => 'datetime',
        'closed_at' => 'datetime',
        'due_at' => 'datetime',
        'evidence_jsonb' => 'array',
        'first_seen_at' => 'datetime',
        'in_progress_at' => 'datetime',
        'last_seen_at' => 'datetime',
        'reopened_at' => 'datetime',
        'resolved_at' => 'datetime',
        'sla_days' => 'integer',
        'times_seen' => 'integer',
        'triaged_at' => 'datetime',
    ];

    public function tenant(): BelongsTo
    {
        return $this->belongsTo(ManagedEnvironment::class, 'managed_environment_id');
    }

    public function baselineRun(): BelongsTo
    {
        return $this->belongsTo(OperationRun::class, 'baseline_operation_run_id');
    }

    public function currentRun(): BelongsTo
    {
        return $this->belongsTo(OperationRun::class, 'current_operation_run_id');
    }

    public function acknowledgedByUser(): BelongsTo
    {
        return $this->belongsTo(User::class, 'acknowledged_by_user_id');
    }

    public function ownerUser(): BelongsTo
    {
        return $this->belongsTo(User::class, 'owner_user_id');
    }

    public function assigneeUser(): BelongsTo
    {
        return $this->belongsTo(User::class, 'assignee_user_id');
    }

    public function closedByUser(): BelongsTo
    {
        return $this->belongsTo(User::class, 'closed_by_user_id');
    }

    /**
     * @return HasOne<FindingException, $this>
     */
    public function findingException(): HasOne
    {
        return $this->hasOne(FindingException::class);
    }

    /**
     * @return array<int, string>
     */
    public static function openStatuses(): array
    {
        return [
            self::STATUS_NEW,
            self::STATUS_TRIAGED,
            self::STATUS_IN_PROGRESS,
            self::STATUS_REOPENED,
        ];
    }

    /**
     * @return array<int, string>
     */
    public static function terminalStatuses(): array
    {
        return [
            self::STATUS_RESOLVED,
            self::STATUS_CLOSED,
            self::STATUS_RISK_ACCEPTED,
        ];
    }

    /**
     * @return array<int, string>
     */
    public static function openStatusesForQuery(): array
    {
        return self::openStatuses();
    }

    /**
     * @return array<int, string>
     */
    public static function highSeverityValues(): array
    {
        return [
            self::SEVERITY_HIGH,
            self::SEVERITY_CRITICAL,
        ];
    }

    /**
     * @return array<int, string>
     */
    public static function manualResolveReasonKeys(): array
    {
        return [
            self::RESOLVE_REASON_REMEDIATED,
        ];
    }

    /**
     * @return array<int, string>
     */
    public static function systemResolveReasonKeys(): array
    {
        return [
            self::RESOLVE_REASON_NO_LONGER_DRIFTING,
            self::RESOLVE_REASON_PERMISSION_GRANTED,
            self::RESOLVE_REASON_PERMISSION_REMOVED_FROM_REGISTRY,
            self::RESOLVE_REASON_ROLE_ASSIGNMENT_REMOVED,
            self::RESOLVE_REASON_GA_COUNT_WITHIN_THRESHOLD,
        ];
    }

    /**
     * @return array<int, string>
     */
    public static function resolveReasonKeys(): array
    {
        return [
            ...self::manualResolveReasonKeys(),
            ...self::systemResolveReasonKeys(),
        ];
    }

    /**
     * @return array<int, string>
     */
    public static function closeReasonKeys(): array
    {
        return [
            self::CLOSE_REASON_FALSE_POSITIVE,
            self::CLOSE_REASON_DUPLICATE,
            self::CLOSE_REASON_NO_LONGER_APPLICABLE,
            self::CLOSE_REASON_ACCEPTED_RISK,
        ];
    }

    /**
     * @return array<int, string>
     */
    public static function manualCloseReasonKeys(): array
    {
        return [
            self::CLOSE_REASON_FALSE_POSITIVE,
            self::CLOSE_REASON_DUPLICATE,
            self::CLOSE_REASON_NO_LONGER_APPLICABLE,
        ];
    }

    /**
     * @return array<int, string>
     */
    public static function reopenReasonKeys(): array
    {
        return [
            self::REOPEN_REASON_RECURRED_AFTER_RESOLUTION,
            self::REOPEN_REASON_VERIFICATION_FAILED,
            self::REOPEN_REASON_MANUAL_REASSESSMENT,
        ];
    }

    public static function isResolveReason(?string $reason): bool
    {
        return is_string($reason) && in_array($reason, self::resolveReasonKeys(), true);
    }

    public static function isManualResolveReason(?string $reason): bool
    {
        return is_string($reason) && in_array($reason, self::manualResolveReasonKeys(), true);
    }

    public static function isSystemResolveReason(?string $reason): bool
    {
        return is_string($reason) && in_array($reason, self::systemResolveReasonKeys(), true);
    }

    public static function isCloseReason(?string $reason): bool
    {
        return is_string($reason) && in_array($reason, self::closeReasonKeys(), true);
    }

    public static function isManualCloseReason(?string $reason): bool
    {
        return is_string($reason) && in_array($reason, self::manualCloseReasonKeys(), true);
    }

    public static function isRiskAcceptedReason(?string $reason): bool
    {
        return $reason === self::CLOSE_REASON_ACCEPTED_RISK;
    }

    public static function isReopenReason(?string $reason): bool
    {
        return is_string($reason) && in_array($reason, self::reopenReasonKeys(), true);
    }

    public static function canonicalizeStatus(?string $status): ?string
    {
        return $status;
    }

    public static function isOpenStatus(?string $status): bool
    {
        return is_string($status) && in_array($status, self::openStatusesForQuery(), true);
    }

    public static function isTerminalStatus(?string $status): bool
    {
        $canonical = self::canonicalizeStatus($status);

        return is_string($canonical) && in_array($canonical, self::terminalStatuses(), true);
    }

    public function hasOpenStatus(): bool
    {
        return self::isOpenStatus($this->status);
    }

    public function isRiskAccepted(): bool
    {
        return (string) $this->status === self::STATUS_RISK_ACCEPTED;
    }

    public function resolve(string $reason): self
    {
        $this->forceFill([
            'status' => self::STATUS_RESOLVED,
            'resolved_at' => now(),
            'resolved_reason' => $reason,
        ]);

        $this->save();

        return $this;
    }

    /**
     * @param  array<string, mixed>  $evidence
     */
    public function reopen(array $evidence): self
    {
        $this->forceFill([
            'status' => self::STATUS_NEW,
            'resolved_at' => null,
            'resolved_reason' => null,
            'evidence_jsonb' => $evidence,
        ]);

        $this->save();

        return $this;
    }

    public function resolvedSubjectDisplayName(): ?string
    {
        $displayName = $this->getAttribute('subject_display_name');

        if (is_string($displayName) && trim($displayName) !== '') {
            return trim($displayName);
        }

        $fallback = Arr::get($this->evidence_jsonb ?? [], 'display_name');
        $fallback = is_string($fallback) ? trim($fallback) : null;

        return $fallback !== '' ? $fallback : null;
    }

    public function artifactSourceDescriptor(): ArtifactSourceDescriptor
    {
        return app(ArtifactSourceResolver::class)->forFinding($this);
    }

    public function artifactProviderDetail(): ArtifactProviderDetail
    {
        return app(ArtifactSourceResolver::class)->providerDetailForFinding($this);
    }

    public function responsibilityState(): string
    {
        if ($this->owner_user_id === null) {
            return self::RESPONSIBILITY_STATE_ORPHANED_ACCOUNTABILITY;
        }

        if ($this->assignee_user_id === null) {
            return self::RESPONSIBILITY_STATE_OWNED_UNASSIGNED;
        }

        return self::RESPONSIBILITY_STATE_ASSIGNED;
    }

    public function hasAccountabilityGap(): bool
    {
        return $this->responsibilityState() === self::RESPONSIBILITY_STATE_ORPHANED_ACCOUNTABILITY;
    }

    public function responsibilityStateLabel(): string
    {
        return match ($this->responsibilityState()) {
            self::RESPONSIBILITY_STATE_ORPHANED_ACCOUNTABILITY => 'orphaned accountability',
            self::RESPONSIBILITY_STATE_OWNED_UNASSIGNED => 'owned but unassigned',
            default => 'assigned',
        };
    }

    public function scopeWithSubjectDisplayName(Builder $query): Builder
    {
        return $query->addSelect([
            'subject_display_name' => InventoryItem::query()
                ->select('display_name')
                ->whereColumn('inventory_items.managed_environment_id', 'findings.managed_environment_id')
                ->whereColumn('inventory_items.external_id', 'findings.subject_external_id')
                ->limit(1),
        ]);
    }

    public function scopeOpenWorkflow(Builder $query): Builder
    {
        return $query->whereIn('status', self::openStatusesForQuery());
    }

    public function scopeDrift(Builder $query): Builder
    {
        return $query->where('finding_type', self::FINDING_TYPE_DRIFT);
    }

    public function scopeOpenDrift(Builder $query): Builder
    {
        return $query
            ->drift()
            ->openWorkflow();
    }

    public function scopeBaselineCompareForProfile(Builder $query, BaselineProfile|int $profile): Builder
    {
        $profileId = $profile instanceof BaselineProfile
            ? (int) $profile->getKey()
            : (int) $profile;

        return $query
            ->drift()
            ->where('source', 'baseline.compare')
            ->where('scope_key', 'baseline_profile:'.$profileId);
    }

    public function scopeOverdueOpen(Builder $query): Builder
    {
        return $query
            ->openWorkflow()
            ->whereNotNull('due_at')
            ->where('due_at', '<', now());
    }

    public function scopeHighSeverity(Builder $query): Builder
    {
        return $query->whereIn('severity', self::highSeverityValues());
    }

    public function scopeHighSeverityActive(Builder $query): Builder
    {
        return $query
            ->openWorkflow()
            ->highSeverity();
    }
}
