<?php

namespace DirectoryTree\ImapEngine;

use DirectoryTree\ImapEngine\Connection\Responses\Data\ListData;
use DirectoryTree\ImapEngine\Connection\Tokens\Nil;
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
use DirectoryTree\ImapEngine\Support\MimeParameterParser;
use DirectoryTree\ImapEngine\Support\Str;
use Illuminate\Contracts\Support\Arrayable;
use JsonSerializable;

class BodyStructurePart implements Arrayable, JsonSerializable
{
    /**
     * Constructor.
     */
    public function __construct(
        protected string $partNumber,
        protected string $type,
        protected string $subtype,
        protected array $parameters = [],
        protected ?string $id = null,
        protected ?string $description = null,
        protected ?string $encoding = null,
        protected ?int $size = null,
        protected ?int $lines = null,
        protected ?ContentDisposition $disposition = null,
    ) {}

    /**
     * Parse a single part BODYSTRUCTURE ListData into a BodyStructurePart.
     */
    public static function fromListData(ListData $data, string $partNumber = '1'): static
    {
        return static::parse($data->tokens(), $partNumber);
    }

    /**
     * Parse a single (non-multipart) part.
     *
     * @param  array<Token|ListData>  $tokens
     */
    protected static function parse(array $tokens, string $partNumber): static
    {
        return new static(
            partNumber: $partNumber,
            type: strtolower(static::tokenValueAt($tokens, 0) ?? 'text'),
            subtype: strtolower(static::tokenValueAt($tokens, 1) ?? 'plain'),
            parameters: isset($tokens[2]) && $tokens[2] instanceof ListData
                ? MimeParameterParser::parse($tokens[2]->toKeyValuePairs())
                : [],
            id: static::tokenValueAt($tokens, 3),
            description: is_null($description = static::tokenValueAt($tokens, 4))
                 ? null
                 : Str::decodeMimeHeader($description),
            encoding: static::tokenValueAt($tokens, 5),
            size: static::tokenIntValueAt($tokens, 6),
            lines: static::tokenIntValueAt($tokens, 7),
            disposition: ContentDisposition::parse($tokens),
        );
    }

    /**
     * Safely read a scalar token value from the parsed body structure.
     *
     * @param  array<Token|ListData>  $tokens
     */
    protected static function tokenValueAt(array $tokens, int $index): ?string
    {
        if (is_null($token = $tokens[$index] ?? null)) {
            return null;
        }

        if (! $token instanceof Token || $token instanceof Nil) {
            return null;
        }

        return $token->value;
    }

    /**
     * Safely read an integer token value from the parsed body structure.
     *
     * @param  array<Token|ListData>  $tokens
     */
    protected static function tokenIntValueAt(array $tokens, int $index): ?int
    {
        $value = static::tokenValueAt($tokens, $index);

        return is_null($value) ? null : (int) $value;
    }

    /**
     * Get the part number (e.g., "1", "1.2", "2.1.3").
     */
    public function partNumber(): string
    {
        return $this->partNumber;
    }

    /**
     * Get the MIME type (e.g., "text", "image", "multipart").
     */
    public function type(): string
    {
        return $this->type;
    }

    /**
     * Get the MIME subtype (e.g., "plain", "html", "jpeg", "mixed").
     */
    public function subtype(): string
    {
        return $this->subtype;
    }

    /**
     * Get the full content type (e.g., "text/plain", "multipart/alternative").
     */
    public function contentType(): string
    {
        return "{$this->type}/{$this->subtype}";
    }

    /**
     * Get the parameters (e.g., charset, boundary).
     */
    public function parameters(): array
    {
        return $this->parameters;
    }

    /**
     * Get a specific parameter value.
     */
    public function parameter(string $name): ?string
    {
        return $this->parameters[strtolower($name)] ?? null;
    }

    /**
     * Get the content ID.
     */
    public function id(): ?string
    {
        return $this->id;
    }

    /**
     * Get the content description.
     */
    public function description(): ?string
    {
        return $this->description;
    }

    /**
     * Get the content transfer encoding.
     */
    public function encoding(): ?string
    {
        return $this->encoding;
    }

    /**
     * Get the size in bytes.
     */
    public function size(): ?int
    {
        return $this->size;
    }

    /**
     * Get the number of lines (for text parts).
     */
    public function lines(): ?int
    {
        return $this->lines;
    }

    /**
     * Get the content disposition.
     */
    public function disposition(): ?ContentDisposition
    {
        return $this->disposition;
    }

    /**
     * Get the filename from disposition parameters.
     */
    public function filename(): ?string
    {
        return $this->disposition?->filename() ?? $this->parameter('name');
    }

    /**
     * Get the charset from parameters.
     */
    public function charset(): ?string
    {
        return $this->parameters['charset'] ?? null;
    }

    /**
     * Determine if this is a text part.
     */
    public function isText(): bool
    {
        return $this->type === 'text' && $this->subtype === 'plain';
    }

    /**
     * Determine if this is an HTML part.
     */
    public function isHtml(): bool
    {
        return $this->type === 'text' && $this->subtype === 'html';
    }

    /**
     * Determine if this is an attachment.
     */
    public function isAttachment(): bool
    {
        if ($this->disposition?->isAttachment()) {
            return true;
        }

        // Inline parts are not attachments.
        if ($this->disposition?->isInline()) {
            return false;
        }

        // Consider non-text/html parts with filenames as attachments.
        if ($this->filename() && ! $this->isText() && ! $this->isHtml()) {
            return true;
        }

        return false;
    }

    /**
     * Determine if this is an inline part.
     */
    public function isInline(): bool
    {
        return $this->disposition?->isInline() ?? false;
    }

    /**
     * Get the array representation.
     */
    public function toArray(): array
    {
        return [
            'id' => $this->id,
            'type' => $this->type,
            'size' => $this->size,
            'lines' => $this->lines,
            'subtype' => $this->subtype,
            'encoding' => $this->encoding,
            'parameters' => $this->parameters,
            'part_number' => $this->partNumber,
            'description' => $this->description,
            'content_type' => $this->contentType(),
            'disposition' => $this->disposition?->toArray(),
        ];
    }

    /**
     * Get the JSON representation.
     */
    public function jsonSerialize(): array
    {
        return $this->toArray();
    }
}
