import fs from 'fs';
import os from 'os';
import path from 'path';
import { fileURLToPath } from 'url';
import readline from 'readline';
import { Command, Option } from 'commander';
import { FileSystemConfigStore } from './store/file-system-config-store.js';
import { SecureConfigStore } from './store/secure-config-store.js';
import { GmailClientFactory } from './client/gmail-client.js';
import { OutlookClientFactory } from './client/outlook-client.js';
import { ImapClientFactory } from './client/imap-client.js';
import { TrashCleanerFactory } from './trash-cleaner.js';
import { ActionLog } from './utils/action-log.js';
import type { ConfigStore } from './store/config-store.js';
import type { EmailClient } from './client/email-client.js';
import type { TrashCleaner } from './trash-cleaner.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
const version: string = pkg.version;

const EmailService = {
    IMAP: 'imap',
    GMAIL: 'gmail',
    OUTLOOK: 'outlook'
} as const;

const PATH_CONFIG = path.join(os.homedir(), '.config', 'trash-cleaner');

// Sample files bundled with the package
const SAMPLE_DIR = path.join(__dirname, '..', 'config');
const SAMPLE_FILES: Record<string, string> = {
    'keywords.yaml': 'keywords.yaml.sample',
    'llm-providers.yaml': 'llm-providers.yaml.sample',
    'allowlist.yaml': 'allowlist.yaml.sample',
    'imap.credentials.json': 'imap.credentials.json.sample',
    'gmail.credentials.json': 'gmail.credentials.json.sample',
    'outlook.credentials.json': 'outlook.credentials.sample.json'
};

interface ValidationIssue {
    file: string;
    level: 'ok' | 'error' | 'info';
    message: string;
}

/**
 * A command line interface for the trash cleaner.
 */
class Cli {
    private _cmd: Command;

    constructor() {
        this._cmd = new Command();
        this._cmd.version(version);
        this._cmd
            .addOption(
                new Option('-r, --reconfig', 'reconfigures the auth for a service'))
            .addOption(
                new Option('-t, --dry-run', 'perform a dry-run cleanup without deleting the emails'))
            .addOption(
                new Option('-q, --quiet', 'suppress spinner and verbose output (for cron/scripts)'))
            .addOption(
                new Option('-i, --interactive', 'preview matches and confirm before acting'))
            .addOption(
                new Option('-d, --debug', 'output extra debugging info'))
            .addOption(
                new Option('-l, --launch', 'launch the auth url in the browser'))
            .addOption(
                new Option('-c, --configDirPath <path>',
                    'the path to config directory')
                    .default(PATH_CONFIG))
            .addOption(
                new Option('-s, --service <service>',
                    'the email service to use')
                    .default(EmailService.IMAP)
                    .choices(Object.values(EmailService)))
            .addOption(
                new Option('-a, --account <name>',
                    'the account name for multi-account support')
                    .default('default'))
            .addOption(
                new Option('-f, --format <format>',
                    'output format for the report')
                    .default('text')
                    .choices(['text', 'html']))
            .addOption(
                new Option('-m, --min-age <days>',
                    'only process emails older than N days')
                    .argParser(parseInt));

        this._cmd.addHelpText('after', `
Commands:
  login                           save credentials securely in OS keychain
  logout                          remove credentials from OS keychain
  init [configDir]                initialize config directory with sample files
  validate [configDir]            validate keywords config file
  list-rules [configDir]          list all configured keyword rules
  undo [configDir]                undo last action using the action log`);
    }

    /**
     * The entry point for the command line interface.
     */
    async run(args: string[]): Promise<boolean> {
        // Handle 'init' subcommand before Commander parsing
        const initIndex = args.indexOf('init');
        if (initIndex >= 2) {
            const configDirPath = args[initIndex + 1] || PATH_CONFIG;
            return this._initConfig(configDirPath);
        }

        // Handle 'list-rules' subcommand before Commander parsing
        const listRulesIndex = args.indexOf('list-rules');
        if (listRulesIndex >= 2) {
            const configDirPath = args[listRulesIndex + 1] || PATH_CONFIG;
            return this._listRules(configDirPath);
        }

        // Handle 'undo' subcommand before Commander parsing
        const undoIndex = args.indexOf('undo');
        if (undoIndex >= 2) {
            const configDirPath = args[undoIndex + 1] || PATH_CONFIG;
            return this._undo(configDirPath, args);
        }

        // Handle 'validate' subcommand before Commander parsing
        const validateIndex = args.indexOf('validate');
        if (validateIndex >= 2) {
            const configDirPath = args[validateIndex + 1] || PATH_CONFIG;
            return this._validate(configDirPath);
        }

        // Handle 'login' subcommand before Commander parsing
        const loginIndex = args.indexOf('login');
        if (loginIndex >= 2) {
            return this._login(args);
        }

        // Handle 'logout' subcommand before Commander parsing
        const logoutIndex = args.indexOf('logout');
        if (logoutIndex >= 2) {
            return this._logout(args);
        }

        this._cmd.parse(args);
        const options = this._cmd.opts();

        if (!fs.existsSync(options.configDirPath)) {
            console.error(`Config directory not found: ${options.configDirPath}\nRun 'trash-cleaner init' to set up your configuration.`);
            return false;
        }

        try {
            const configStore = this._createConfigStore(options.configDirPath);
            const client = await this._createEmailClient(configStore,
                options.service,
                options.reconfig,
                options.launch,
                options.account);
            const actionLog = new ActionLog(options.configDirPath);
            const trashCleanerFactory = new TrashCleanerFactory(configStore,
                client,
                !options.quiet,
                !!options.quiet,
                options.format,
                actionLog,
                options.minAge);
            const trashCleaner = await trashCleanerFactory.getInstance();

            if (options.interactive) {
                await this._runInteractive(trashCleaner);
            } else {
                await trashCleaner.cleanTrash(!!options.dryRun);
            }
        }
        catch (err: unknown) {
            const error = err as Error;
            if (options.debug) {
                console.error('An error occurred:', err);
            }
            else {
                console.error(error.message);
            }
            return false;
        }

        return true;
    }

    /**
     * Initializes the config directory with sample files.
     */
    _initConfig(configDirPath: string): boolean {
        if (!fs.existsSync(configDirPath)) {
            fs.mkdirSync(configDirPath, { recursive: true });
            console.log(`Created config directory: ${configDirPath}`);
        } else {
            console.log(`Config directory already exists: ${configDirPath}`);
        }

        let copiedCount = 0;
        for (const [targetName, sampleName] of Object.entries(SAMPLE_FILES)) {
            const targetPath = path.join(configDirPath, targetName);
            const samplePath = path.join(SAMPLE_DIR, sampleName);

            if (fs.existsSync(targetPath)) {
                console.log(`  Skipped ${targetName} (already exists)`);
            } else if (!fs.existsSync(samplePath)) {
                console.log(`  Skipped ${targetName} (sample not found)`);
            } else {
                fs.copyFileSync(samplePath, targetPath);
                console.log(`  Created ${targetName}`);
                copiedCount++;
            }
        }

        console.log('');
        if (copiedCount > 0) {
            console.log('Next steps:');
            console.log(`  1. Edit ${path.join(configDirPath, 'keywords.yaml')} to configure your keyword rules`);
            console.log(`  2. Run: trash-cleaner login   (saves credentials securely in OS keychain)`);
            console.log(`     or edit IMAP/Gmail/Outlook credential files for file-based setup`);
            console.log(`  3. Run: trash-cleaner -c ${configDirPath}`);
        } else {
            console.log('All config files already exist. Edit them as needed.');
        }

        return true;
    }

    /**
     * Lists active keyword rules from the config.
     */
    async _listRules(configDirPath: string): Promise<boolean> {
        try {
            const configStore = new FileSystemConfigStore(configDirPath);
            const factory = new TrashCleanerFactory(configStore, {} as EmailClient, false);
            const { keywords } = await factory.readKeywords();

            console.log(`Rules loaded from: ${configDirPath}`);
            console.log(`Total rules: ${keywords.length}`);
            console.log('');

            keywords.forEach((keyword, index) => {
                const action = keyword.action || 'delete';
                const fields = keyword.fields.join(', ');
                const labels = keyword.labels.join(', ');
                console.log(`  ${index + 1}. /${keyword.value}/`);
                console.log(`     Fields: ${fields} | Labels: ${labels} | Action: ${action}`);
            });

            // Show allowlist if present
            const allowlist = await factory.readAllowlist();
            if (allowlist.length > 0) {
                console.log('');
                console.log(`Allowlist (${allowlist.length} pattern${allowlist.length === 1 ? '' : 's'}):`);
                allowlist.forEach((pattern, index) => {
                    console.log(`  ${index + 1}. /${pattern}/`);
                });
            }
        } catch (err: unknown) {
            console.error((err as Error).message);
            return false;
        }

        return true;
    }

    /**
     * Shows the last action batch and offers to undo it.
     */
    async _undo(configDirPath: string, args: string[]): Promise<boolean> {
        const actionLog = new ActionLog(configDirPath);
        const batch = actionLog.getLastBatch();

        if (!batch) {
            console.log('No actions to undo.');
            return true;
        }

        console.log(`\nLast action (${batch.timestamp}):\n`);
        batch.entries.forEach((entry, i) => {
            console.log(`  ${i + 1}. [${entry.action}] ${entry.from} — ${entry.subject}`);
        });
        console.log('');

        const confirmed = await this._confirm(
            `Restore ${batch.entries.length} email(s)? (y/N) `
        );

        if (!confirmed) {
            console.log('Cancelled.');
            return true;
        }

        // Determine service and account from args
        const serviceIndex = args.indexOf('-s') !== -1 ? args.indexOf('-s') : args.indexOf('--service');
        const service = serviceIndex !== -1 ? args[serviceIndex + 1] : EmailService.IMAP;

        if (service === EmailService.IMAP) {
            console.error('Undo is not supported in IMAP mode. Use --service gmail or --service outlook for undo support.');
            return false;
        }

        const accountIndex = args.indexOf('-a') !== -1 ? args.indexOf('-a') : args.indexOf('--account');
        const account = accountIndex !== -1 ? args[accountIndex + 1] : undefined;

        try {
            const configStore = this._createConfigStore(configDirPath);
            const client = await this._createEmailClient(configStore, service!, false, false, account);

            const emailIds = batch.entries.map(e => e.id);
            await (client as any).restoreEmails(emailIds);

            actionLog.removeLastBatch();
            console.log(`Restored ${batch.entries.length} email(s).`);
        } catch (err: unknown) {
            console.error(`Undo failed: ${(err as Error).message}`);
            return false;
        }

        return true;
    }

    /**
     * Validates configuration files and reports any issues.
     */
    async _validate(configDirPath: string): Promise<boolean> {
        const issues: ValidationIssue[] = [];
        let hasErrors = false;

        // Check config directory exists
        if (!fs.existsSync(configDirPath)) {
            console.error(`Config directory not found: ${configDirPath}`);
            console.log('Run "trash-cleaner init" to create it.');
            return false;
        }

        // Check keywords config (yaml or json)
        const keywordsYaml = path.join(configDirPath, 'keywords.yaml');
        const keywordsJson = path.join(configDirPath, 'keywords.json');
        const keywordsFile = fs.existsSync(keywordsYaml) ? 'keywords.yaml' : 'keywords.json';
        if (!fs.existsSync(keywordsYaml) && !fs.existsSync(keywordsJson)) {
            issues.push({ file: 'keywords.yaml', level: 'error', message: 'File not found (required)' });
            hasErrors = true;
        } else {
            try {
                const configStore = new FileSystemConfigStore(configDirPath);
                const factory = new TrashCleanerFactory(configStore, {} as EmailClient, false);
                const { keywords } = await factory.readKeywords();
                issues.push({ file: keywordsFile, level: 'ok', message: `${keywords.length} rule(s) loaded` });
            } catch (err: unknown) {
                issues.push({ file: keywordsFile, level: 'error', message: (err as Error).message });
                hasErrors = true;
            }
        }

        // Check allowlist (optional, yaml or json)
        const allowlistYaml = path.join(configDirPath, 'allowlist.yaml');
        const allowlistJson = path.join(configDirPath, 'allowlist.json');
        if (fs.existsSync(allowlistYaml) || fs.existsSync(allowlistJson)) {
            const allowlistFile = fs.existsSync(allowlistYaml) ? 'allowlist.yaml' : 'allowlist.json';
            try {
                const configStore = new FileSystemConfigStore(configDirPath);
                const factory = new TrashCleanerFactory(configStore, {} as EmailClient, false);
                const allowlist = await factory.readAllowlist();
                for (const pattern of allowlist) {
                    new RegExp(pattern, 'i');
                }
                issues.push({ file: allowlistFile, level: 'ok', message: `${allowlist.length} pattern(s) loaded` });
            } catch (err: unknown) {
                issues.push({ file: fs.existsSync(allowlistYaml) ? 'allowlist.yaml' : 'allowlist.json', level: 'error', message: (err as Error).message });
                hasErrors = true;
            }
        } else {
            issues.push({ file: 'allowlist.yaml', level: 'info', message: 'Not found (optional)' });
        }

        // Check llm-providers (optional, yaml or json)
        const llmYaml = path.join(configDirPath, 'llm-providers.yaml');
        const llmJson = path.join(configDirPath, 'llm-providers.json');
        if (fs.existsSync(llmYaml) || fs.existsSync(llmJson)) {
            const llmFile = fs.existsSync(llmYaml) ? 'llm-providers.yaml' : 'llm-providers.json';
            try {
                const configStore = new FileSystemConfigStore(configDirPath);
                const factory = new TrashCleanerFactory(configStore, {} as EmailClient, false);
                const providers = await factory.readLlmProviders();
                const count = Object.keys(providers).length;
                issues.push({ file: llmFile, level: 'ok', message: `${count} provider(s) configured` });
            } catch (err: unknown) {
                issues.push({ file: fs.existsSync(llmYaml) ? 'llm-providers.yaml' : 'llm-providers.json', level: 'error', message: (err as Error).message });
                hasErrors = true;
            }
        } else {
            issues.push({ file: 'llm-providers.yaml', level: 'info', message: 'Not found (needed for LLM rules)' });
        }

        // Check credential files
        for (const credFile of ['imap.credentials.json', 'gmail.credentials.json', 'outlook.credentials.json']) {
            const credPath = path.join(configDirPath, credFile);
            if (fs.existsSync(credPath)) {
                try {
                    JSON.parse(fs.readFileSync(credPath, 'utf8'));
                    issues.push({ file: credFile, level: 'ok', message: 'Valid JSON' });
                } catch {
                    issues.push({ file: credFile, level: 'error', message: 'Invalid JSON' });
                    hasErrors = true;
                }
            } else {
                issues.push({ file: credFile, level: 'info', message: 'Not found (needed for service)' });
            }
        }

        // Print results
        console.log(`\nValidating config: ${configDirPath}\n`);
        for (const issue of issues) {
            const icon = issue.level === 'ok' ? '✓' : issue.level === 'error' ? '✗' : '–';
            console.log(`  ${icon} ${issue.file}: ${issue.message}`);
        }
        console.log('');

        if (hasErrors) {
            console.log('Validation failed. Fix the errors above.');
        } else {
            console.log('Configuration is valid.');
        }

        return !hasErrors;
    }

    /**
     * Creates a ConfigStore with keychain support for secure credential storage.
     */
    _createConfigStore(configDirPath: string): ConfigStore {
        const fileStore = new FileSystemConfigStore(configDirPath);
        return new SecureConfigStore(fileStore);
    }

    /**
     * Creates a readline interface for interactive prompts.
     */
    _createReadlineInterface(): readline.Interface {
        return readline.createInterface({
            input: process.stdin,
            output: process.stdout
        });
    }

    /**
     * Saves credentials to the OS keychain for a service.
     */
    async _login(args: string[]): Promise<boolean> {
        const service = this._getArgValue(args, '-s', '--service') || EmailService.IMAP;
        const account = this._getArgValue(args, '-a', '--account') || 'default';

        const rl = this._createReadlineInterface();
        const ask = (question: string): Promise<string> => new Promise(resolve =>
            rl.question(question, resolve));

        try {
            let credentials: object;
            let keychainKey: string;

            switch (service) {
                case EmailService.IMAP: {
                    const suffix = (!account || account === 'default') ? '' : `.${account}`;
                    keychainKey = `imap.credentials${suffix}.json`;
                    const host = await ask('IMAP host (e.g., imap.gmail.com): ');
                    if (!host.trim()) {
                        console.error('Error: IMAP host is required.');
                        return false;
                    }
                    const port = await ask('IMAP port (default: 993): ');
                    const user = await ask('Email address: ');
                    if (!user.trim()) {
                        console.error('Error: Email address is required.');
                        return false;
                    }
                    const password = await ask('App password: ');
                    if (!password.trim()) {
                        console.error('Error: App password is required.');
                        return false;
                    }
                    const archiveFolder = await ask('Archive folder (default: Archive): ');
                    credentials = {
                        host: host.trim(),
                        port: parseInt(port) || 993,
                        user: user.trim(),
                        password,
                        archiveFolder: archiveFolder.trim() || undefined
                    };
                    break;
                }
                case EmailService.GMAIL: {
                    const suffix = (!account || account === 'default') ? '' : `.${account}`;
                    keychainKey = `gmail.credentials${suffix}.json`;
                    console.log('Paste your Gmail OAuth2 credentials JSON (from Google Cloud Console):');
                    const json = await ask('> ');
                    if (!json.trim()) {
                        console.error('Error: OAuth2 credentials JSON is required.');
                        return false;
                    }
                    credentials = JSON.parse(json);
                    break;
                }
                case EmailService.OUTLOOK: {
                    const suffix = (!account || account === 'default') ? '' : `.${account}`;
                    keychainKey = `outlook.credentials${suffix}.json`;
                    const clientId = await ask('Client ID: ');
                    if (!clientId.trim()) {
                        console.error('Error: Client ID is required.');
                        return false;
                    }
                    const tenantId = await ask('Tenant ID: ');
                    if (!tenantId.trim()) {
                        console.error('Error: Tenant ID is required.');
                        return false;
                    }
                    const aadEndpoint = await ask('AAD endpoint (default: https://login.microsoftonline.com/): ');
                    const graphEndpoint = await ask('Graph endpoint (default: https://graph.microsoft.com/): ');
                    credentials = {
                        client_id: clientId.trim(),
                        tenant_id: tenantId.trim(),
                        aad_endpoint: aadEndpoint.trim() || 'https://login.microsoftonline.com/',
                        graph_endpoint: graphEndpoint.trim() || 'https://graph.microsoft.com/'
                    };
                    break;
                }
                default:
                    console.error(`Unknown service: ${service}`);
                    return false;
            }

            const { SecureConfigStore: SC } = await import('./store/secure-config-store.js');
            const store = new SC({ get: () => null, put: () => {} } as any);
            await store.putJson(keychainKey!, credentials);

            console.log(`\n✓ Credentials saved to OS keychain for ${service} (account: ${account})`);
            console.log('  Your credentials are stored securely and will not be written to disk.');
            if (!fs.existsSync(PATH_CONFIG)) {
                console.log(`\nNext step: run 'trash-cleaner init' to create your config directory with keyword rules.`);
            }
            return true;
        } catch (err: unknown) {
            console.error(`Login failed: ${(err as Error).message}`);
            return false;
        } finally {
            rl.close();
        }
    }

    /**
     * Removes credentials from the OS keychain for a service.
     */
    async _logout(args: string[]): Promise<boolean> {
        const service = this._getArgValue(args, '-s', '--service') || EmailService.IMAP;
        const account = this._getArgValue(args, '-a', '--account') || 'default';
        const suffix = (!account || account === 'default') ? '' : `.${account}`;

        const { SecureConfigStore: SC } = await import('./store/secure-config-store.js');
        const store = new SC({ get: () => null, put: () => {} } as any);

        const keys: string[] = [];

        switch (service) {
            case EmailService.IMAP:
                keys.push(`imap.credentials${suffix}.json`);
                break;
            case EmailService.GMAIL:
                keys.push(`gmail.credentials${suffix}.json`);
                keys.push(`gmail.token${suffix}.json`);
                break;
            case EmailService.OUTLOOK:
                keys.push(`outlook.credentials${suffix}.json`);
                keys.push(`outlook.token${suffix}.json`);
                break;
            default:
                console.error(`Unknown service: ${service}`);
                return false;
        }

        let removed = 0;
        for (const key of keys) {
            if (await store.remove(key)) {
                removed++;
            }
        }

        if (removed > 0) {
            console.log(`✓ Removed ${removed} credential(s) from OS keychain for ${service} (account: ${account})`);
        } else {
            console.log(`No keychain credentials found for ${service} (account: ${account})`);
        }

        return true;
    }

    /**
     * Gets a CLI argument value by short or long flag.
     */
    _getArgValue(args: string[], shortFlag: string, longFlag: string): string | undefined {
        const index = args.indexOf(shortFlag) !== -1 ? args.indexOf(shortFlag) : args.indexOf(longFlag);
        return index !== -1 ? args[index + 1] : undefined;
    }

    /**
     * Creates an instance of email client by service name.
     */
    async _createEmailClient(configStore: ConfigStore, service: string, reconfig: boolean, launch: boolean, account: string = 'default'): Promise<EmailClient> {
        let factory = null;
        switch (service) {
            case EmailService.IMAP:
                factory = new ImapClientFactory(configStore, account);
                break;
            case EmailService.GMAIL:
                factory = new GmailClientFactory(configStore, account);
                break;
            case EmailService.OUTLOOK:
                factory = new OutlookClientFactory(configStore, account);
                break;
            default:
                throw new Error(`Email service '${service}' not yet implemented.`);
        }
        return await factory.getInstance(reconfig, launch);
    }

    /**
     * Runs the cleaner in interactive mode: preview matches, then confirm.
     */
    async _runInteractive(trashCleaner: TrashCleaner): Promise<void> {
        const emails = await trashCleaner.findTrash();

        if (emails.length === 0) {
            console.log('No trash emails found.');
            return;
        }

        console.log(`\nFound ${emails.length} trash email(s):\n`);

        const confirmed = [];
        let bulk: 'yes' | 'no' | null = null;
        for (let i = 0; i < emails.length; i++) {
            const email = emails[i]!;
            const action = email._action || 'delete';
            const rule = email._rule ? `  Rule: ${email._rule}\n` : '';
            console.log(`  ${i + 1}/${emails.length} [${action}] ${email.from} — ${email.subject}`);
            if (rule) {
                console.log(rule.trimEnd());
            }

            if (bulk === 'yes') {
                confirmed.push(email);
                continue;
            }
            if (bulk === 'no') {
                continue;
            }

            const answer = await this._promptAction(`  ${action}? (y/n/Y=yes all/N=no all) `);
            if (answer === 'yes-all') {
                bulk = 'yes';
                confirmed.push(email);
            } else if (answer === 'no-all') {
                bulk = 'no';
            } else if (answer === 'yes') {
                confirmed.push(email);
            }
        }

        if (confirmed.length === 0) {
            console.log('No emails selected.');
            return;
        }

        console.log(`\nProcessing ${confirmed.length} of ${emails.length} email(s)...`);
        await trashCleaner.processEmails(confirmed);
        console.log('Done.');
    }

    /**
     * Prompts the user for an interactive action choice.
     * Accepts: y (yes), n (no), Y (yes all), N (no all).
     */
    _promptAction(question: string): Promise<string> {
        const rl = readline.createInterface({
            input: process.stdin,
            output: process.stdout
        });

        return new Promise(resolve => {
            rl.question(question, (answer) => {
                rl.close();
                const trimmed = answer.trim();
                if (trimmed === 'Y') {
                    resolve('yes-all');
                } else if (trimmed === 'N') {
                    resolve('no-all');
                } else if (trimmed.toLowerCase() === 'y' || trimmed.toLowerCase() === 'yes') {
                    resolve('yes');
                } else {
                    resolve('no');
                }
            });
        });
    }

    /**
     * Prompts the user for yes/no confirmation.
     */
    _confirm(question: string): Promise<boolean> {
        const rl = readline.createInterface({
            input: process.stdin,
            output: process.stdout
        });

        return new Promise(resolve => {
            rl.question(question, (answer) => {
                rl.close();
                resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
            });
        });
    }
}

export { Cli };
