use std::collections::HashMap;

use log::debug;
use rowan::TextRange;
use rue_ast::{AstGenericArguments, AstNode, AstPathSegment};
use rue_diagnostic::DiagnosticKind;
use rue_hir::{Declaration, ImportId, Symbol, SymbolId};
use rue_parser::SyntaxToken;
use rue_types::{Apply, Type, TypeId};

use crate::{Compiler, CompletionContext, GetTextRange, SyntaxItemKind, compile_generic_arguments};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PathKind {
    Symbol,
    Type,
}

#[derive(Debug, Clone, Copy)]
pub enum PathResult {
    Unresolved,
    Symbol(SymbolId, Option<TypeId>, Option<ImportId>),
    Type(TypeId, Option<ImportId>),
}

pub trait PathSegment {
    fn separator(&self) -> Option<TextRange>;
    fn text_range(&self) -> TextRange;
    fn name(&self) -> Option<SyntaxToken>;
    fn generic_arguments(&self) -> Option<AstGenericArguments>;
}

impl PathSegment for AstPathSegment {
    fn separator(&self) -> Option<TextRange> {
        self.separator().map(|separator| separator.text_range())
    }

    fn text_range(&self) -> TextRange {
        self.syntax().text_range()
    }

    fn name(&self) -> Option<SyntaxToken> {
        self.name()
    }

    fn generic_arguments(&self) -> Option<AstGenericArguments> {
        self.generic_arguments()
    }
}

impl PathSegment for SyntaxToken {
    fn separator(&self) -> Option<TextRange> {
        None
    }

    fn text_range(&self) -> TextRange {
        self.text_range()
    }

    fn name(&self) -> Option<SyntaxToken> {
        Some(self.clone())
    }

    fn generic_arguments(&self) -> Option<AstGenericArguments> {
        None
    }
}

pub fn compile_path<S>(
    ctx: &mut Compiler,
    range: &impl GetTextRange,
    segments: impl Iterator<Item = S>,
    kind: PathKind,
    mut completions: bool,
) -> PathResult
where
    S: PathSegment,
{
    let mut segments = segments.collect::<Vec<_>>();
    let mut base_scope = ctx.last_scope_id();
    let mut module_stack = ctx.parent_module_stack().to_vec();
    let mut path_completion_module = None;

    let length = segments.len();

    if let Some(segment) = segments.first()
        && let Some(separator) = segment.separator()
    {
        ctx.diagnostic(&separator, DiagnosticKind::PathSeparatorInFirstSegment);
    }

    while let Some(segment) = segments.first()
        && let Some(name) = segment.name()
        && name.text() == "super"
    {
        if let Some(module) = path_completion_module
            && completions
            && let Some(separator) = segment.separator()
        {
            ctx.add_syntax(
                SyntaxItemKind::CompletionContext(CompletionContext::ModuleExports {
                    module,
                    allow_super: true,
                }),
                TextRange::new(separator.end(), segment.text_range().end()),
            );
        }

        segments.remove(0);

        if let Some(module) = module_stack.pop() {
            base_scope = ctx.module(module).scope;
            path_completion_module = Some(module);

            ctx.add_syntax(SyntaxItemKind::SymbolReference(module), name.text_range());
        } else {
            ctx.diagnostic(&name, DiagnosticKind::UnresolvedSuper);
            completions = false;
        }
    }

    let mut value = None;
    let mut previous_name = None;

    for (index, segment) in segments.iter().enumerate() {
        if let Some(module) = path_completion_module
            && completions
            && let Some(separator) = segment.separator()
        {
            ctx.add_syntax(
                SyntaxItemKind::CompletionContext(CompletionContext::ModuleExports {
                    module,
                    allow_super: index == 0,
                }),
                TextRange::new(separator.end(), segment.text_range().end()),
            );
        }

        if let Some(name) = segment.name()
            && name.text() == "super"
        {
            if index == length - 1 {
                ctx.diagnostic(&name, DiagnosticKind::SuperAtEnd);
            } else {
                ctx.diagnostic(&name, DiagnosticKind::SuperAfterNamedImport);
            }
            return PathResult::Unresolved;
        }

        let Some(name) = segment.name() else {
            return PathResult::Unresolved;
        };

        let (symbol, ty) = if let Some(value) = value {
            if let PathResult::Symbol(symbol, _, _) = value
                && let Symbol::Module(module) = ctx.symbol(symbol)
            {
                let scope = ctx.scope(module.scope);
                let symbol = scope.symbol(name.text()).map(|symbol| {
                    (
                        symbol,
                        scope.is_symbol_exported(symbol),
                        scope.symbol_import(symbol),
                    )
                });
                let ty = scope
                    .ty(name.text())
                    .map(|ty| (ty, scope.is_type_exported(ty), scope.type_import(ty)));
                (symbol, ty)
            } else {
                ctx.diagnostic(
                    &name,
                    DiagnosticKind::SubpathNotSupported(previous_name.unwrap()),
                );
                return PathResult::Unresolved;
            }
        } else if base_scope == ctx.last_scope_id() {
            let symbol = ctx
                .resolve_symbol_in(base_scope, name.text())
                .map(|(symbol, import)| (symbol, true, import));
            let ty = ctx
                .resolve_type_in(base_scope, name.text())
                .map(|(ty, import)| (ty, true, import));
            (symbol, ty)
        } else {
            let base = ctx.scope(base_scope);
            let symbol = base
                .symbol(name.text())
                .filter(|s| base.is_symbol_exported(*s))
                .map(|s| (s, true, base.symbol_import(s)));
            let ty = base
                .ty(name.text())
                .filter(|t| base.is_type_exported(*t))
                .map(|t| (t, true, base.type_import(t)));
            (symbol, ty)
        };

        let initial = match (symbol, ty) {
            (Some((symbol, _, symbol_import)), Some((ty, _, type_import))) => match kind {
                PathKind::Symbol => PathResult::Symbol(symbol, None, symbol_import),
                PathKind::Type => PathResult::Type(ty, type_import),
            },
            (Some((symbol, _, symbol_import)), None) => {
                PathResult::Symbol(symbol, None, symbol_import)
            }
            (None, Some((ty, _, type_import))) => PathResult::Type(ty, type_import),
            (None, None) => {
                match kind {
                    PathKind::Symbol => ctx.diagnostic(
                        &name,
                        DiagnosticKind::UndeclaredSymbol(name.text().to_string()),
                    ),
                    PathKind::Type => ctx.diagnostic(
                        &name,
                        DiagnosticKind::UndeclaredType(name.text().to_string()),
                    ),
                }
                PathResult::Unresolved
            }
        };

        if matches!(initial, PathResult::Symbol(_, _, _))
            && let Some((_, is_exported, _)) = symbol
            && !is_exported
        {
            ctx.diagnostic(
                &name,
                DiagnosticKind::PrivateSymbol(
                    name.text().to_string(),
                    previous_name.clone().unwrap(),
                ),
            );
        }

        if matches!(initial, PathResult::Type(_, _))
            && let Some((_, is_exported, _)) = ty
            && !is_exported
        {
            ctx.diagnostic(
                &name,
                DiagnosticKind::PrivateType(
                    name.text().to_string(),
                    previous_name.clone().unwrap(),
                ),
            );
        }

        match initial {
            PathResult::Unresolved => return PathResult::Unresolved,
            PathResult::Symbol(symbol, mut override_type, import) => {
                ctx.reference(Declaration::Symbol(symbol), import);
                ctx.reference_span(Declaration::Symbol(symbol), name.text_range());

                let args = if let Some(generic_arguments) = segment.generic_arguments() {
                    compile_generic_arguments(ctx, &generic_arguments)
                } else {
                    vec![]
                };

                let ty = ctx.symbol_type(symbol);

                if !args.is_empty() {
                    if let Symbol::Function(function) = ctx.symbol(symbol).clone() {
                        if function.vars.len() != args.len() {
                            ctx.diagnostic(
                                &name,
                                DiagnosticKind::ExpectedGenericArguments(
                                    function.vars.len(),
                                    args.len(),
                                ),
                            );
                        }

                        let mut map = HashMap::new();

                        for (i, arg) in args.iter().enumerate() {
                            if i >= function.vars.len() {
                                break;
                            }

                            map.insert(function.vars[i], *arg);
                        }

                        override_type = Some(ctx.alloc_type(Type::Apply(Apply::new(ty, map))));
                    } else {
                        ctx.diagnostic(&name, DiagnosticKind::GenericArgumentsOnSymbolReference);
                        return PathResult::Unresolved;
                    }
                }

                if matches!(ctx.symbol(symbol), Symbol::Module(_)) {
                    path_completion_module = Some(symbol);
                }

                value = Some(PathResult::Symbol(symbol, override_type, import));
            }
            PathResult::Type(mut ty, import) => {
                ctx.reference(Declaration::Type(ty), import);
                ctx.reference_span(Declaration::Type(ty), name.text_range());

                let args = if let Some(generic_arguments) = segment.generic_arguments() {
                    compile_generic_arguments(ctx, &generic_arguments)
                } else {
                    vec![]
                };

                let semantic = rue_types::unwrap_semantic(ctx.types_mut(), ty, false);

                let params = match ctx.ty(semantic) {
                    Type::Apply(_) | Type::Ref(_) => unreachable!(),
                    Type::Unresolved => return PathResult::Unresolved,
                    Type::Atom(..)
                    | Type::Pair(..)
                    | Type::Function(..)
                    | Type::Generic(_)
                    | Type::Union(..)
                    | Type::Never
                    | Type::Any => {
                        vec![]
                    }
                    Type::Alias(alias) => alias.generics.clone(),
                    Type::Struct(ty) => ty.generics.clone(),
                };

                if params.len() != args.len() {
                    ctx.diagnostic(
                        &name,
                        DiagnosticKind::ExpectedGenericArguments(params.len(), args.len()),
                    );
                }

                if !params.is_empty() {
                    let mut map = HashMap::new();
                    for (i, param) in params.into_iter().enumerate() {
                        let arg = args.get(i).copied().unwrap_or_else(|| {
                            debug!("Unresolved type due to missing generic argument");
                            ctx.builtins().unresolved.ty
                        });
                        map.insert(param, arg);
                    }
                    ty = ctx.alloc_type(Type::Apply(Apply::new(ty, map)));
                }

                value = Some(PathResult::Type(ty, import));
            }
        }

        previous_name = Some(name.text().to_string());
    }

    let value = value.unwrap_or(PathResult::Unresolved);

    match (value, kind) {
        (PathResult::Symbol(_, _, _), PathKind::Type) => {
            ctx.diagnostic(range, DiagnosticKind::ExpectedType(previous_name.unwrap()));
            PathResult::Unresolved
        }
        (PathResult::Type(_, _), PathKind::Symbol) => {
            ctx.diagnostic(
                range,
                DiagnosticKind::ExpectedSymbol(previous_name.unwrap()),
            );
            PathResult::Unresolved
        }
        (PathResult::Symbol(symbol, _, _), PathKind::Symbol) => {
            if let Symbol::Module(_) = ctx.symbol(symbol) {
                ctx.diagnostic(
                    range,
                    DiagnosticKind::CannotReferenceModule(previous_name.unwrap()),
                );
            }
            value
        }
        _ => value,
    }
}
