import { ImportacionLayoutColumn, LayoutColumnaType } from '@win2win/shared';
import { fromPairs, isDate, isNil, keys, mapKeys, snakeCase, values } from 'lodash';
import * as XLSX from 'xlsx';

const keyFormatFn = (key: string) => snakeCase(key)
export type RowContent = Record<string, string | number | Date>
export interface ParsedRow {
    code: string,
    line: number,
    content: RowContent
}
export interface ValidRow extends ParsedRow {
    valid: true
}
export interface InvalidRow extends ParsedRow {
    errors: RowErrors,
    valid: false
}
export type AnyRow = ValidRow | InvalidRow

export type RowErrors = { detail: RowErrorsDetail, count: number }
type RowErrorsDetail = Record<string, string[]>
const EMPTY_VALUE = ''
export class FileReaderService {

    async getStructure(file: File): Promise<ImportacionLayoutColumn[]> {
        const data = await this.read(file);
        const columns: Record<string, ImportacionLayoutColumn> = {}
        data.forEach(item => {
            const headers = keys(item).filter(k => isNaN(Number(k)));
            headers.forEach(header => {
                const key = this.generateKey(header)
                const value = (typeof item[header]) === 'string' ? item[header].trim() : item[header]
                if (!(key in columns)) {
                    columns[key] = {
                        type: this.getValueType(value),
                        primary: false,
                        required: true,
                        header,
                        key,
                    }
                }
                const savedKeys = keys(columns)
                savedKeys.forEach(k => {
                    if (!headers.includes(columns[k].header) || isNil(value) || value === '') {
                        columns[k].required = false
                    }
                })
            })
        })
        return values(columns)
    }

    async getStructuredFile(file: File, structure: ImportacionLayoutColumn[]): Promise<{ validRows: ValidRow[], invalidRows: InvalidRow[] }> {
        const structureObj = fromPairs(structure.map(s => [s.key, s]))
        const rawRows = await this.read(file);
        const validRows: ValidRow[] = []
        const invalidRows: InvalidRow[] = []
        const _keys = structure.map(s => s.key)
        rawRows.forEach((item, index) => {
            const content: RowContent = mapKeys(item, (_, key) => this.generateKey(key))
            const code = this.generateCode(content, structure)
            const _item: ParsedRow = { line: index + 1, code, content }
            _keys.forEach(key => {
                _item[key] = content[key] ?? EMPTY_VALUE
            })
            const rowErrors: RowErrors = { detail: {}, count: 0 }
            _keys.forEach(key => {
                const config = structureObj[key]
                const value = this.getValue(_item[key], config.type)
                const keyErrors = this.getRowErrors(config, value)
                if (keyErrors.length) {
                    rowErrors.detail[key] = keyErrors
                }
            })

            if (validRows.some(r => r.code === code)) {
                rowErrors.detail['$code'] = ['código duplicado']
            }
            rowErrors.count = this.getErrorsCount(rowErrors.detail)
            if (rowErrors.count) {
                invalidRows.push({ ..._item, errors: rowErrors, valid: false } as InvalidRow)
            } else {
                validRows.push({ ..._item, valid: true } as ValidRow)
            }
        })
        return {
            validRows,
            invalidRows
        }
    }

    private generateCode(row: RowContent, structure: ImportacionLayoutColumn[]): string {
        const primaryKey = structure.find(s => s.primary)?.key;
        const primaryKeyValue = row[primaryKey ?? '']
        const code = primaryKeyValue ? String(primaryKeyValue) : ''
        return code
    }

    private getErrorsCount(errors: RowErrorsDetail): number {
        return values(errors).reduce((acc, curr) => acc + curr.length, 0)
    }

    private getValue(value: any, type: LayoutColumnaType) {
        if (isNil(value) || value === '') return EMPTY_VALUE;
        switch (type) {
            case 'fecha':
                return new Date(value)
            case 'numero':
                return Number(value)
            default:
                return String(value)
        }
    }

    private read(file: File) {
        return new Promise<any[]>((resolve, reject) => {
            let jsonData: any;
            const reader = new FileReader();
            reader.onload = (e) => {
                const result = (e.target as FileReader)?.result;
                if (!result) reject();
                const workbook = XLSX.read(result, { cellDates: true });
                const sheetName: string = workbook.SheetNames[0];
                const ws = workbook.Sheets[sheetName];
                jsonData = XLSX.utils.sheet_to_json(ws, {
                    raw: true,
                    blankrows: false,
                });
                try {
                    resolve(jsonData);
                } catch (e) {
                    reject(e);
                }
            };
            reader.readAsArrayBuffer(file);
        });
    }

    private getRowErrors(config: ImportacionLayoutColumn, value: any) {
        const errors: string[] = []
        if (config.required && value === EMPTY_VALUE) {
            errors.push('campo requerido')
        }
        if (config.type === 'fecha' && !isDate(value)) {
            errors.push('debe ser una fecha')
        }
        if (config.type === 'numero' && isNaN(value as number)) {
            errors.push('debe ser un número')
        }
        // este no es necesario porque antes de llegar aca ya se convirtio a string
        // if (config.type === 'texto' && typeof value !== 'string') {
        //     errors.push('debe ser un texto')
        // }
        return errors
    }

    private getValueType(value: any): LayoutColumnaType {
        if (isDate(value)) return 'fecha';
        if (typeof value === 'number') return 'numero'
        return 'texto'
    }

    private generateKey(key: string) {
        return keyFormatFn(key)
    }
}
