import { LitElement, html, css, nothing } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import '@awesome.me/webawesome/dist/components/button/button.js';
import '@awesome.me/webawesome/dist/components/input/input.js';
import '@awesome.me/webawesome/dist/components/select/select.js';
import '@awesome.me/webawesome/dist/components/option/option.js';
import '@awesome.me/webawesome/dist/components/switch/switch.js';
import '@awesome.me/webawesome/dist/components/spinner/spinner.js';
import '@awesome.me/webawesome/dist/components/callout/callout.js';
import { designTokens } from '../../styles/tokens.css';
import type {
    DownloadDescriptor,
    DownloadProvider,
    ProviderField,
} from '@store/download-store';
import { downloadStore } from '@store/download-store';
import { GetDownloadPreferences, SetDownloadPreferences } from '@go/config/config.js';
import { SetPreferences } from '@go/download/service.js';
import type * as download from '@go/download/models.js';
import { Format } from '@go/download/models.js';
import { compact } from '@utils/binding';
import { describeError, explainError } from '@utils/describe-error';
import { confirmAction } from '@components/confirm-dialog/confirm-dialog';
import './config-section';
import { pickDirectory } from '../../utils/pick-directory';

/**
 * Allowed audio formats for auto-download, mirrored from
 * backend/download/types.go's `Format` constants. `FormatUnknown` is
 * deliberately excluded — it names "no format detected", not a format a
 * user could opt into.
 */
const AUTO_DOWNLOAD_FORMATS: { value: Format; label: string }[] = [
    { value: Format.FormatFLAC, label: 'FLAC' },
    { value: Format.FormatALAC, label: 'ALAC' },
    { value: Format.FormatWAV, label: 'WAV' },
    { value: Format.FormatMP3, label: 'MP3' },
    { value: Format.FormatAAC, label: 'AAC' },
    { value: Format.FormatOGG, label: 'OGG' },
    { value: Format.FormatOpus, label: 'Opus' },
    { value: Format.FormatWMA, label: 'WMA' },
];

/**
 * Download client configuration.
 *
 * The forms are rendered from the descriptors the backend publishes, not
 * from anything hard-coded here, so adding a provider on the backend
 * gives it a settings UI with no frontend change. That is also why
 * secret fields render as password inputs purely on the descriptor's
 * say-so — the frontend never needs to know which services have keys.
 */
@customElement('download-clients')
export class DownloadClients extends LitElement {
    @state()
    private providers: DownloadProvider[] = [];

    @state()
    private descriptors: DownloadDescriptor[] = [];

    /** Provider being edited, or 'new' while adding one. */
    @state()
    private editing: number | 'new' | null = null;

    /** Kind selected in the add form. */
    @state()
    private newKind = '';

    /** Working copy of the form's field values. */
    @state()
    private draft: Record<string, string> = {};

    @state()
    private draftName = '';

    /** Per-provider connection test results, keyed by provider ID. */
    @state()
    private testResults: Record<number, { ok: boolean; message: string }> = {};

    @state()
    private testing: number | null = null;

    @state()
    private errorMessage = '';

    /** Working copy of the auto-download guardrails. */
    @state()
    private prefs: download.AutoDownloadPrefs = {
        minKbps: 0,
        maxKbps: 0,
        preferredKbps: 0,
        maxSizeMb: 0,
        allowedFormats: [],
    } as download.AutoDownloadPrefs;

    @state()
    private prefsSaving = false;

    @state()
    private prefsError = '';

    @state()
    private prefsSaved = false;

    private unsubscribe: (() => void) | null = null;

    override connectedCallback(): void {
        super.connectedCallback();

        this.unsubscribe = downloadStore.subscribe(() => this.syncFromStore());

        void downloadStore.init().then(() => this.syncFromStore());
        void this.loadPreferences();
    }

    override disconnectedCallback(): void {
        super.disconnectedCallback();

        this.unsubscribe?.();
        this.unsubscribe = null;
    }

    private syncFromStore(): void {
        this.providers = downloadStore.providers;
        this.descriptors = downloadStore.descriptors;
    }

    private async loadPreferences(): Promise<void> {
        try {
            this.prefs = await GetDownloadPreferences();
        } catch (err) {
            console.error('Failed to load auto-download preferences:', err);
        }
    }

    static override styles = [
        designTokens,
        css`
            :host {
                display: block;
            }

            .clients {
                display: flex;
                flex-direction: column;
                gap: 0.6em;
            }

            .client {
                display: grid;
                grid-template-columns: 1fr auto;
                gap: 0.75em;
                align-items: center;
                padding: 0.7em 0.85em;
                border: 1px solid var(--wa-color-surface-border, #333);
                border-radius: 8px;
            }

            .client-name {
                font-weight: 600;
            }

            .client-meta {
                font-size: 0.82em;
                opacity: 0.7;
                margin-top: 0.15em;
            }

            .client-actions {
                display: flex;
                gap: 0.4em;
                align-items: center;
            }

            .test-result {
                font-size: 0.8em;
                margin-top: 0.35em;
            }

            .test-result.ok {
                color: var(--wa-color-success-fill-loud, #4c9f70);
            }

            .test-result.fail {
                color: var(--wa-color-danger-fill-loud, #c65f5f);
            }

            .form {
                display: flex;
                flex-direction: column;
                gap: 0.7em;
                padding: 0.9em;
                border: 1px solid var(--wa-color-surface-border, #333);
                border-radius: 8px;
                margin-top: 0.6em;
            }

            .form-actions {
                display: flex;
                gap: 0.5em;
                justify-content: flex-end;
                margin-top: 0.3em;
            }

            .requires {
                font-size: 0.82em;
                opacity: 0.75;
            }

            .empty {
                opacity: 0.7;
                font-size: 0.9em;
                padding: 0.5em 0;
            }

            .add-row {
                margin-top: 0.8em;
            }

            .field-row {
                display: flex;
                gap: 0.5em;
                align-items: flex-end;
            }

            .field-row wa-input {
                flex: 1;
            }

            .field-row .browse-button {
                flex-shrink: 0;
            }

            .format-options {
                display: flex;
                flex-wrap: wrap;
                gap: 0.4em 1em;
                margin-top: 0.4em;
            }

            .format-option {
                display: flex;
                align-items: center;
                gap: 0.4em;
                font-size: 0.9em;
                cursor: pointer;
            }
        `,
    ];

    override render() {
        return html`
            <config-section
                heading="Download Clients"
                description="Connect services you already run to search for and download music. Nothing is enabled until you add a client."
            >
                ${this.errorMessage
                    ? html`<wa-callout variant="danger">${this.errorMessage}</wa-callout>`
                    : nothing}

                <div class="clients">
                    ${this.providers.length === 0 && this.editing !== 'new'
                        ? html`<div class="empty">No download clients connected.</div>`
                        : nothing}
                    ${this.providers.map((provider) => this.renderProvider(provider))}
                </div>

                ${this.editing === 'new'
                    ? this.renderAddForm()
                    : html`
                        <div class="add-row">
                            <wa-button size="small" @click=${this.startAdd}>
                                Add download client
                            </wa-button>
                        </div>
                    `}
            </config-section>

            <config-section
                heading="Auto-download preferences"
                description="Guardrails on what the pipeline may grab without asking — a manual pick is never restricted by these, only automatic ones."
            >
                ${this.prefsError
                    ? html`<wa-callout variant="danger">${this.prefsError}</wa-callout>`
                    : nothing}

                <div class="form">
                    <!-- Bitrate, not megabytes.  A size means nothing
                         on its own: 300 MB is a generous single and a
                         suspiciously small boxset, and whoever fills
                         this in has no idea which release it will be
                         applied to.  A rate is the same statement
                         divided by how long the music is, so one number
                         holds across an EP and an opera. -->
                    <div class="field-row">
                        <wa-input
                            label="Minimum bitrate (kbps)"
                            type="number"
                            min="0"
                            placeholder="No minimum"
                            .value=${this.prefs.minKbps ? String(this.prefs.minKbps) : ''}
                            @input=${(e: Event) => {
                                this.prefs = {
                                    ...this.prefs,
                                    minKbps: Number((e.target as HTMLInputElement).value) || 0,
                                };
                            }}
                        ></wa-input>
                        <wa-input
                            label="Maximum bitrate (kbps)"
                            type="number"
                            min="0"
                            placeholder="No maximum"
                            .value=${this.prefs.maxKbps ? String(this.prefs.maxKbps) : ''}
                            @input=${(e: Event) => {
                                this.prefs = {
                                    ...this.prefs,
                                    maxKbps: Number((e.target as HTMLInputElement).value) || 0,
                                };
                            }}
                        ></wa-input>
                        <wa-input
                            label="Preferred bitrate (kbps)"
                            type="number"
                            min="0"
                            placeholder="No preference"
                            .value=${this.prefs.preferredKbps
                                ? String(this.prefs.preferredKbps)
                                : ''}
                            @input=${(e: Event) => {
                                this.prefs = {
                                    ...this.prefs,
                                    preferredKbps:
                                        Number((e.target as HTMLInputElement).value) || 0,
                                };
                            }}
                        ></wa-input>
                    </div>

                    <div class="requires">
                        320 is the top of MP3; a FLAC rip is usually
                        500–1000 depending on the music. Preferred
                        decides between copies that are otherwise equally
                        good — it never rules one out, which is what the
                        minimum and maximum are for.
                    </div>

                    <div class="field-row">
                        <wa-input
                            label="Never grab more than (MB)"
                            type="number"
                            min="0"
                            placeholder="No limit"
                            .value=${this.prefs.maxSizeMb ? String(this.prefs.maxSizeMb) : ''}
                            @input=${(e: Event) => {
                                this.prefs = {
                                    ...this.prefs,
                                    maxSizeMb: Number((e.target as HTMLInputElement).value) || 0,
                                };
                            }}
                        ></wa-input>
                    </div>

                    <div class="requires">
                        A ceiling on the download itself, in case a
                        mislabelled boxset gets through. Still a size
                        because it is a question about disk space, and
                        because it has to apply to a candidate whose
                        bitrate cannot be worked out at all.
                    </div>

                    <div>
                        <div class="requires">
                            Allowed formats — leave all unchecked to allow any format.
                        </div>
                        <div class="format-options">
                            ${AUTO_DOWNLOAD_FORMATS.map(
                                (format) => html`
                                    <label class="format-option">
                                        <input
                                            type="checkbox"
                                            .checked=${(this.prefs.allowedFormats ?? []).includes(
                                                format.value,
                                            )}
                                            @change=${(e: Event) =>
                                                this.toggleFormat(
                                                    format.value,
                                                    (e.target as HTMLInputElement).checked,
                                                )}
                                        />
                                        ${format.label}
                                    </label>
                                `,
                            )}
                        </div>
                    </div>

                    <div class="form-actions">
                        ${this.prefsSaved
                            ? html`<span class="test-result ok">Saved.</span>`
                            : nothing}
                        <wa-button
                            size="small"
                            variant="brand"
                            ?disabled=${this.prefsSaving}
                            @click=${this.savePreferences}
                        >
                            ${this.prefsSaving
                                ? html`<wa-spinner></wa-spinner>`
                                : 'Save preferences'}
                        </wa-button>
                    </div>
                </div>
            </config-section>
        `;
    }

    private renderProvider(provider: DownloadProvider) {
        const descriptor = this.descriptorFor(provider.kind);
        const test = this.testResults[provider.id];

        if (this.editing === provider.id) {
            return this.renderEditForm(provider);
        }

        return html`
            <div class="client">
                <div>
                    <div class="client-name">${provider.name}</div>
                    <div class="client-meta">
                        ${descriptor?.name ?? provider.kind} ·
                        ${provider.enabled ? 'Enabled' : 'Disabled'} ·
                        priority ${provider.priority}
                    </div>
                    ${test
                        ? html`<div class="test-result ${test.ok ? 'ok' : 'fail'}">
                              ${test.message}
                          </div>`
                        : nothing}
                </div>
                <div class="client-actions">
                    <wa-button
                        size="small"
                        appearance="plain"
                        ?disabled=${this.testing === provider.id}
                        @click=${() => this.testProvider(provider)}
                    >
                        ${this.testing === provider.id
                            ? html`<wa-spinner></wa-spinner>`
                            : 'Test'}
                    </wa-button>
                    <wa-button
                        size="small"
                        appearance="plain"
                        @click=${() => this.startEdit(provider)}
                    >
                        Edit
                    </wa-button>
                    <wa-button
                        size="small"
                        appearance="plain"
                        variant="danger"
                        @click=${() => void this.deleteProvider(provider)}
                    >
                        Remove
                    </wa-button>
                </div>
            </div>
        `;
    }

    private renderAddForm() {
        const descriptor = this.descriptorFor(this.newKind);

        return html`
            <div class="form">
                <wa-select
                    label="Client type"
                    .value=${this.newKind}
                    @change=${this.onKindChange}
                >
                    ${this.descriptors.map(
                        (d) => html`<wa-option value=${d.kind}>${d.name}</wa-option>`,
                    )}
                </wa-select>

                ${descriptor
                    ? html`
                        <div class="requires">
                            ${descriptor.summary}
                            ${descriptor.requiresExternal
                                ? html`<br />Requires a running
                                      ${descriptor.requiresExternal} instance.`
                                : nothing}
                        </div>

                        <wa-input
                            label="Name"
                            .value=${this.draftName}
                            @input=${(e: Event) => {
                                this.draftName = (e.target as HTMLInputElement).value;
                            }}
                        ></wa-input>

                        ${this.renderFields(descriptor)}
                    `
                    : nothing}

                <div class="form-actions">
                    <wa-button size="small" appearance="plain" @click=${this.cancelEdit}>
                        Cancel
                    </wa-button>
                    <wa-button
                        size="small"
                        variant="brand"
                        ?disabled=${!descriptor}
                        @click=${this.saveNew}
                    >
                        Add
                    </wa-button>
                </div>
            </div>
        `;
    }

    private renderEditForm(provider: DownloadProvider) {
        const descriptor = this.descriptorFor(provider.kind);

        return html`
            <div class="form">
                <wa-input
                    label="Name"
                    .value=${this.draftName}
                    @input=${(e: Event) => {
                        this.draftName = (e.target as HTMLInputElement).value;
                    }}
                ></wa-input>

                ${descriptor ? this.renderFields(descriptor, provider) : nothing}

                <wa-input
                    label="Priority"
                    type="number"
                    .value=${String(provider.priority)}
                    @input=${(e: Event) => {
                        this.draft['__priority'] = (e.target as HTMLInputElement).value;
                    }}
                ></wa-input>

                <wa-switch
                    ?checked=${provider.enabled}
                    @change=${(e: Event) => {
                        this.draft['__enabled'] = (e.target as HTMLInputElement)
                            .checked
                            ? '1'
                            : '';
                    }}
                >
                    Enabled
                </wa-switch>

                <div class="form-actions">
                    <wa-button size="small" appearance="plain" @click=${this.cancelEdit}>
                        Cancel
                    </wa-button>
                    <wa-button
                        size="small"
                        variant="brand"
                        @click=${() => this.saveEdit(provider)}
                    >
                        Save
                    </wa-button>
                </div>
            </div>
        `;
    }

    /** Renders one input per descriptor field, plus a folder browse
     * button for path fields and an "already set" placeholder for
     * secrets the provider already has a stored value for. */
    private renderFields(descriptor: DownloadDescriptor, provider?: DownloadProvider) {
        return (descriptor.fields ?? []).map((field) => {
            const isSet = field.secret && provider?.setSecrets?.[field.key];
            const placeholder = isSet
                ? '•••••••• (unchanged — enter a new value to replace it)'
                : (field.placeholder ?? '');

            return html`
                <div class="field-row">
                    <wa-input
                        label=${field.label}
                        placeholder=${placeholder}
                        type=${field.secret ? 'password' : 'text'}
                        .value=${this.draft[field.key] ?? ''}
                        @input=${(e: Event) => {
                            this.draft = {
                                ...this.draft,
                                [field.key]: (e.target as HTMLInputElement).value,
                            };
                        }}
                    >
                        ${field.help ? html`<span slot="hint">${field.help}</span>` : nothing}
                    </wa-input>
                    ${field.path
                        ? html`
                            <wa-button
                                size="small"
                                appearance="outlined"
                                class="browse-button"
                                @click=${() => this.browseForFolder(field)}
                            >
                                Browse
                            </wa-button>
                        `
                        : nothing}
                </div>
            `;
        });
    }

    private browseForFolder = async (field: ProviderField) => {
        try {
            const dir = await pickDirectory();

            if (dir) {
                this.draft = { ...this.draft, [field.key]: dir };
            }
        } catch (err) {
            console.error('Failed to open directory picker:', err);
        }
    };

    private descriptorFor(kind: string): DownloadDescriptor | undefined {
        return this.descriptors.find((d) => d.kind === kind);
    }

    private startAdd = () => {
        this.editing = 'new';
        this.errorMessage = '';
        this.newKind = this.descriptors[0]?.kind ?? '';
        this.draftName = this.descriptorFor(this.newKind)?.name ?? '';
        this.draft = this.defaultsFor(this.newKind);
    };

    private startEdit(provider: DownloadProvider) {
        this.editing = provider.id;
        this.errorMessage = '';
        this.draftName = provider.name;
        // Secrets are never sent back to the frontend, so their fields
        // start blank; a blank secret on save means "leave it alone"
        // rather than "clear it".
        this.draft = compact(provider.settings);
    }

    private cancelEdit = () => {
        this.editing = null;
        this.draft = {};
        this.errorMessage = '';
    };

    private onKindChange = (event: Event) => {
        this.newKind = (event.target as HTMLInputElement).value;
        this.draftName = this.descriptorFor(this.newKind)?.name ?? '';
        this.draft = this.defaultsFor(this.newKind);
    };

    private defaultsFor(kind: string): Record<string, string> {
        const descriptor = this.descriptorFor(kind);
        const out: Record<string, string> = {};

        for (const field of descriptor?.fields ?? []) {
            if (field.default) out[field.key] = field.default;
        }

        return out;
    }

    private saveNew = async () => {
        this.errorMessage = '';

        try {
            await downloadStore.addProvider(
                this.newKind,
                this.draftName || this.newKind,
                this.cleanDraft(),
            );

            this.cancelEdit();
        } catch (err) {
            console.error('Failed to add download client:', err);
            this.errorMessage = explainError(
                err,
                'That client could not be saved.',
            );
        }
    };

    private async saveEdit(provider: DownloadProvider) {
        this.errorMessage = '';

        const priority = this.draft['__priority']
            ? Number(this.draft['__priority'])
            : provider.priority;

        const enabled =
            '__enabled' in this.draft
                ? this.draft['__enabled'] === '1'
                : provider.enabled;

        try {
            await downloadStore.updateProvider(
                provider.id,
                this.draftName || provider.name,
                enabled,
                priority,
                this.cleanDraft(),
            );

            this.cancelEdit();
        } catch (err) {
            console.error('Failed to save download client:', err);
            this.errorMessage = explainError(
                err,
                'Those changes could not be saved.',
            );
        }
    }

    /** Strips the form's internal bookkeeping keys before saving. */
    private cleanDraft(): Record<string, string> {
        const out: Record<string, string> = {};

        for (const [key, value] of Object.entries(this.draft)) {
            if (!key.startsWith('__')) out[key] = value;
        }

        return out;
    }

    /**
     * Removing a client discards its stored credentials, which cannot
     * be recovered — and it used to happen on one click (errors.m4).
     */
    private async deleteProvider(provider: DownloadProvider) {
        const ok = await confirmAction({
            title: `Remove “${provider.name}”?`,
            message:
                'YellowJacket will stop using this client for downloads.',
            impact:
                'Its stored credentials are deleted and cannot be recovered.',
            confirmLabel: 'Remove client',
            danger: true,
        });

        if (!ok) return;

        this.errorMessage = '';

        try {
            await downloadStore.deleteProvider(provider.id);
        } catch (err) {
            console.error('Failed to remove download client:', err);
            this.errorMessage = describeError(
                err,
                'That client could not be removed.',
            );
        }
    }

    private async testProvider(provider: DownloadProvider) {
        this.testing = provider.id;

        try {
            await downloadStore.testProvider(provider.id);

            this.testResults = {
                ...this.testResults,
                [provider.id]: { ok: true, message: 'Connected.' },
            };
        } catch (err) {
            // Deliberately verbatim: a connection test's error is the
            // user's debugging tool for a misconfigured client, and is
            // the documented exception to describeError() (errors.M9).
            this.testResults = {
                ...this.testResults,
                [provider.id]: { ok: false, message: String(err) },
            };
        } finally {
            this.testing = null;
        }
    }

    private toggleFormat(format: Format, checked: boolean): void {
        const current = this.prefs.allowedFormats ?? [];
        const allowedFormats = checked
            ? [...current, format]
            : current.filter((f) => f !== format);

        this.prefs = { ...this.prefs, allowedFormats };
    }

    /**
     * Saves the guardrails both to disk and to the running download
     * manager in one action — persistence alone would leave the setting
     * inert until restart, which is exactly the bug this mirrors away
     * from (see `config.Library`'s prior persist-without-apply gap).
     */
    private savePreferences = async () => {
        this.prefsSaving = true;
        this.prefsError = '';
        this.prefsSaved = false;

        try {
            await SetDownloadPreferences(this.prefs);
            await SetPreferences(this.prefs);
            this.prefsSaved = true;
        } catch (err) {
            console.error('Failed to save download preferences:', err);
            this.prefsError = describeError(
                err,
                'Those preferences could not be saved.',
            );
        } finally {
            this.prefsSaving = false;
        }
    };
}

declare global {
    interface HTMLElementTagNameMap {
        'download-clients': DownloadClients;
    }
}
